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
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//! let spec = DNSZoneSpec {
46//!     zone_name: "example.com".to_string(),
47//!     cluster_ref: Some("my-dns-cluster".to_string()),
48//!     cluster_provider_ref: None,
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_address: "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/// Label selector to match Kubernetes resources.
84///
85/// A label selector is a label query over a set of resources. The result of matchLabels and
86/// matchExpressions are `ANDed`. An empty label selector matches all objects.
87#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
88#[serde(rename_all = "camelCase")]
89pub struct LabelSelector {
90    /// Map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent
91    /// to an element of matchExpressions, whose key field is "key", the operator is "In",
92    /// and the values array contains only "value". All requirements must be satisfied.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub match_labels: Option<BTreeMap<String, String>>,
95
96    /// List of label selector requirements. All requirements must be satisfied.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub match_expressions: Option<Vec<LabelSelectorRequirement>>,
99}
100
101/// A label selector requirement is a selector that contains values, a key, and an operator
102/// that relates the key and values.
103#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "camelCase")]
105pub struct LabelSelectorRequirement {
106    /// The label key that the selector applies to.
107    pub key: String,
108
109    /// Operator represents a key's relationship to a set of values.
110    /// Valid operators are In, `NotIn`, Exists and `DoesNotExist`.
111    pub operator: String,
112
113    /// An array of string values. If the operator is In or `NotIn`,
114    /// the values array must be non-empty. If the operator is Exists or `DoesNotExist`,
115    /// the values array must be empty.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub values: Option<Vec<String>>,
118}
119
120/// Source for DNS records to include in a zone.
121///
122/// Specifies how DNS records should be associated with this zone using label selectors.
123/// Records matching the selector criteria will be automatically included in the zone.
124///
125/// # Example
126///
127/// ```yaml
128/// recordsFrom:
129///   - selector:
130///       matchLabels:
131///         app: podinfo
132///       matchExpressions:
133///         - key: environment
134///           operator: In
135///           values:
136///             - dev
137///             - staging
138/// ```
139#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
140#[serde(rename_all = "camelCase")]
141pub struct RecordSource {
142    /// Label selector to match DNS records.
143    ///
144    /// Records (`ARecord`, `CNAMERecord`, `MXRecord`, etc.) with labels matching this selector
145    /// will be automatically associated with this zone.
146    ///
147    /// The selector uses standard Kubernetes label selector semantics:
148    /// - `matchLabels`: All specified labels must match (AND logic)
149    /// - `matchExpressions`: All expressions must be satisfied (AND logic)
150    /// - Both `matchLabels` and `matchExpressions` can be used together
151    pub selector: LabelSelector,
152}
153
154impl LabelSelector {
155    /// Checks if this label selector matches the given labels.
156    ///
157    /// Returns `true` if all match requirements are satisfied.
158    ///
159    /// # Arguments
160    ///
161    /// * `labels` - The labels to match against (from `metadata.labels`)
162    ///
163    /// # Examples
164    ///
165    /// ```rust
166    /// use std::collections::BTreeMap;
167    /// use bindy::crd::LabelSelector;
168    ///
169    /// let selector = LabelSelector {
170    ///     match_labels: Some(BTreeMap::from([
171    ///         ("app".to_string(), "podinfo".to_string()),
172    ///     ])),
173    ///     match_expressions: None,
174    /// };
175    ///
176    /// let labels = BTreeMap::from([
177    ///     ("app".to_string(), "podinfo".to_string()),
178    ///     ("env".to_string(), "dev".to_string()),
179    /// ]);
180    ///
181    /// assert!(selector.matches(&labels));
182    /// ```
183    #[must_use]
184    pub fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
185        // Check matchLabels (all must match)
186        if let Some(ref match_labels) = self.match_labels {
187            for (key, value) in match_labels {
188                if labels.get(key) != Some(value) {
189                    return false;
190                }
191            }
192        }
193
194        // Check matchExpressions (all must be satisfied)
195        if let Some(ref expressions) = self.match_expressions {
196            for expr in expressions {
197                if !expr.matches(labels) {
198                    return false;
199                }
200            }
201        }
202
203        true
204    }
205}
206
207impl LabelSelectorRequirement {
208    /// Checks if this requirement matches the given labels.
209    ///
210    /// # Arguments
211    ///
212    /// * `labels` - The labels to match against
213    ///
214    /// # Returns
215    ///
216    /// * `true` if the requirement is satisfied, `false` otherwise
217    #[must_use]
218    pub fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
219        match self.operator.as_str() {
220            "In" => {
221                // Label value must be in the values list
222                if let Some(ref values) = self.values {
223                    if let Some(label_value) = labels.get(&self.key) {
224                        values.contains(label_value)
225                    } else {
226                        false
227                    }
228                } else {
229                    false
230                }
231            }
232            "NotIn" => {
233                // Label value must NOT be in the values list
234                if let Some(ref values) = self.values {
235                    if let Some(label_value) = labels.get(&self.key) {
236                        !values.contains(label_value)
237                    } else {
238                        true // Label doesn't exist, so it's not in the list
239                    }
240                } else {
241                    true
242                }
243            }
244            "Exists" => {
245                // Label key must exist (any value)
246                labels.contains_key(&self.key)
247            }
248            "DoesNotExist" => {
249                // Label key must NOT exist
250                !labels.contains_key(&self.key)
251            }
252            _ => false, // Unknown operator
253        }
254    }
255}
256
257/// SOA (Start of Authority) Record specification.
258///
259/// The SOA record defines authoritative information about a DNS zone, including
260/// the primary nameserver, responsible party's email, and timing parameters for
261/// zone transfers and caching.
262///
263/// # Example
264///
265/// ```rust
266/// use bindy::crd::SOARecord;
267///
268/// let soa = SOARecord {
269///     primary_ns: "ns1.example.com.".to_string(),
270///     admin_email: "admin.example.com.".to_string(), // Note: @ replaced with .
271///     serial: 2024010101,
272///     refresh: 3600,   // Check for updates every hour
273///     retry: 600,      // Retry after 10 minutes on failure
274///     expire: 604800,  // Expire after 1 week
275///     negative_ttl: 86400, // Cache negative responses for 1 day
276/// };
277/// ```
278#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
279#[serde(rename_all = "camelCase")]
280pub struct SOARecord {
281    /// Primary nameserver for this zone (must be a FQDN ending with .).
282    ///
283    /// Example: `ns1.example.com.`
284    #[schemars(regex(
285        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])?)*\.$"
286    ))]
287    pub primary_ns: String,
288
289    /// Email address of the zone administrator (@ replaced with ., must end with .).
290    ///
291    /// Example: `admin.example.com.` for admin@example.com
292    #[schemars(regex(
293        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])?)*\.$"
294    ))]
295    pub admin_email: String,
296
297    /// Serial number for this zone. Typically in YYYYMMDDNN format.
298    /// Secondaries use this to determine if they need to update.
299    ///
300    /// Must be a 32-bit unsigned integer (0 to 4294967295).
301    /// The field is i64 to accommodate the full u32 range.
302    #[schemars(range(min = 0, max = 4_294_967_295_i64))]
303    pub serial: i64,
304
305    /// Refresh interval in seconds. How often secondaries should check for updates.
306    ///
307    /// Typical values: 3600-86400 (1 hour to 1 day).
308    #[schemars(range(min = 1, max = 2_147_483_647))]
309    pub refresh: i32,
310
311    /// Retry interval in seconds. How long to wait before retrying a failed refresh.
312    ///
313    /// Should be less than refresh. Typical values: 600-7200 (10 minutes to 2 hours).
314    #[schemars(range(min = 1, max = 2_147_483_647))]
315    pub retry: i32,
316
317    /// Expire time in seconds. After this time, secondaries stop serving the zone
318    /// if they can't contact the primary.
319    ///
320    /// Should be much larger than refresh+retry. Typical values: 604800-2419200 (1-4 weeks).
321    #[schemars(range(min = 1, max = 2_147_483_647))]
322    pub expire: i32,
323
324    /// Negative caching TTL in seconds. How long to cache NXDOMAIN responses.
325    ///
326    /// Typical values: 300-86400 (5 minutes to 1 day).
327    #[schemars(range(min = 0, max = 2_147_483_647))]
328    pub negative_ttl: i32,
329}
330
331/// Condition represents an observation of a resource's current state.
332///
333/// Conditions are used in status subresources to communicate the state of
334/// a resource to users and controllers.
335#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
336#[serde(rename_all = "camelCase")]
337pub struct Condition {
338    /// Type of condition. Common types include: Ready, Available, Progressing, Degraded, Failed.
339    pub r#type: String,
340
341    /// Status of the condition: True, False, or Unknown.
342    pub status: String,
343
344    /// Brief CamelCase reason for the condition's last transition.
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub reason: Option<String>,
347
348    /// Human-readable message indicating details about the transition.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub message: Option<String>,
351
352    /// Last time the condition transitioned from one status to another (RFC3339 format).
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub last_transition_time: Option<String>,
355}
356
357/// Reference to a DNS record associated with a zone
358#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
359#[serde(rename_all = "camelCase")]
360pub struct RecordReference {
361    /// API version of the record (e.g., "bindy.firestoned.io/v1beta1")
362    pub api_version: String,
363    /// Kind of the record (e.g., `ARecord`, `CNAMERecord`, `MXRecord`)
364    pub kind: String,
365    /// Name of the record resource
366    pub name: String,
367}
368
369/// `DNSZone` status
370#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
371#[serde(rename_all = "camelCase")]
372pub struct DNSZoneStatus {
373    #[serde(default)]
374    pub conditions: Vec<Condition>,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub observed_generation: Option<i64>,
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub record_count: Option<i32>,
379    /// IP addresses of secondary servers configured for zone transfers.
380    /// Used to detect when secondary IPs change and zones need updating.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub secondary_ips: Option<Vec<String>>,
383    /// List of DNS records successfully associated with this zone.
384    /// Updated by the zone reconciler when records are added/removed.
385    #[serde(default, skip_serializing_if = "Vec::is_empty")]
386    pub records: Vec<RecordReference>,
387}
388
389/// Secondary Zone configuration
390#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
391#[serde(rename_all = "camelCase")]
392pub struct SecondaryZoneConfig {
393    /// Primary server addresses for zone transfer
394    pub primary_servers: Vec<String>,
395    /// Optional TSIG key for authenticated transfers
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub tsig_key: Option<String>,
398}
399
400/// `DNSZone` defines a DNS zone to be managed by BIND9.
401///
402/// A `DNSZone` represents an authoritative DNS zone (e.g., example.com) that will be
403/// served by a BIND9 cluster. The zone includes SOA record information and will be
404/// synchronized to all instances in the referenced cluster via AXFR/IXFR.
405///
406/// `DNSZones` can reference either:
407/// - A namespace-scoped `Bind9Cluster` (using `clusterRef`)
408/// - A cluster-scoped `ClusterBind9Provider` (using `clusterProviderRef`)
409///
410/// Exactly one of `clusterRef` or `clusterProviderRef` must be specified.
411///
412/// # Example: Namespace-scoped Cluster
413///
414/// ```yaml
415/// apiVersion: bindy.firestoned.io/v1beta1
416/// kind: DNSZone
417/// metadata:
418///   name: example-com
419///   namespace: dev-team-alpha
420/// spec:
421///   zoneName: example.com
422///   clusterRef: dev-team-dns  # References Bind9Cluster in same namespace
423///   soaRecord:
424///     primaryNs: ns1.example.com.
425///     adminEmail: admin.example.com.
426///     serial: 2024010101
427///     refresh: 3600
428///     retry: 600
429///     expire: 604800
430///     negativeTtl: 86400
431///   ttl: 3600
432/// ```
433///
434/// # Example: Cluster-scoped Global Cluster
435///
436/// ```yaml
437/// apiVersion: bindy.firestoned.io/v1beta1
438/// kind: DNSZone
439/// metadata:
440///   name: production-example-com
441///   namespace: production
442/// spec:
443///   zoneName: example.com
444///   clusterProviderRef: shared-production-dns  # References ClusterBind9Provider (cluster-scoped)
445///   soaRecord:
446///     primaryNs: ns1.example.com.
447///     adminEmail: admin.example.com.
448///     serial: 2024010101
449///     refresh: 3600
450///     retry: 600
451///     expire: 604800
452///     negativeTtl: 86400
453///   ttl: 3600
454/// ```
455#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
456#[kube(
457    group = "bindy.firestoned.io",
458    version = "v1beta1",
459    kind = "DNSZone",
460    namespaced,
461    shortname = "zone",
462    shortname = "zones",
463    shortname = "dz",
464    shortname = "dzs",
465    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.",
466    printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".spec.zoneName"}"#,
467    printcolumn = r#"{"name":"Provider","type":"string","jsonPath":".spec.clusterProviderRef"}"#,
468    printcolumn = r#"{"name":"Records","type":"integer","jsonPath":".status.recordCount"}"#,
469    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl","priority":1}"#,
470    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
471)]
472#[kube(status = "DNSZoneStatus")]
473#[serde(rename_all = "camelCase")]
474pub struct DNSZoneSpec {
475    /// DNS zone name (e.g., "example.com").
476    ///
477    /// Must be a valid DNS zone name. Can be a domain or subdomain.
478    /// Examples: "example.com", "internal.example.com", "10.in-addr.arpa"
479    #[schemars(regex(
480        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])?$"
481    ))]
482    pub zone_name: String,
483
484    /// Reference to a namespace-scoped `Bind9Cluster` in the same namespace.
485    ///
486    /// Must match the name of a `Bind9Cluster` resource in the same namespace.
487    /// The zone will be added to all instances in this cluster.
488    ///
489    /// Either `clusterRef` or `clusterProviderRef` must be specified (not both).
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub cluster_ref: Option<String>,
492
493    /// Reference to a cluster-scoped `ClusterBind9Provider`.
494    ///
495    /// Must match the name of a `ClusterBind9Provider` resource (cluster-scoped).
496    /// The zone will be added to all instances in this provider.
497    ///
498    /// Either `clusterRef` or `clusterProviderRef` must be specified (not both).
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub cluster_provider_ref: Option<String>,
501
502    /// SOA (Start of Authority) record - defines zone authority and refresh parameters.
503    ///
504    /// The SOA record is required for all authoritative zones and contains
505    /// timing information for zone transfers and caching.
506    pub soa_record: SOARecord,
507
508    /// Default TTL (Time To Live) for records in this zone, in seconds.
509    ///
510    /// If not specified, individual records must specify their own TTL.
511    /// Typical values: 300-86400 (5 minutes to 1 day).
512    #[serde(default)]
513    #[schemars(range(min = 0, max = 2_147_483_647))]
514    pub ttl: Option<i32>,
515
516    /// Map of nameserver hostnames to IP addresses for glue records.
517    ///
518    /// Glue records provide IP addresses for nameservers within the zone's own domain.
519    /// This is necessary when delegating subdomains where the nameserver is within the
520    /// delegated zone itself.
521    ///
522    /// Example: When delegating `sub.example.com` with nameserver `ns1.sub.example.com`,
523    /// you must provide the IP address of `ns1.sub.example.com` as a glue record.
524    ///
525    /// Format: `{"ns1.example.com.": "192.0.2.1", "ns2.example.com.": "192.0.2.2"}`
526    ///
527    /// Note: Nameserver hostnames should end with a dot (.) for FQDN.
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub name_server_ips: Option<HashMap<String, String>>,
530
531    /// Sources for DNS records to include in this zone.
532    ///
533    /// This field defines label selectors that automatically associate DNS records with this zone.
534    /// Records with matching labels will be included in the zone's DNS configuration.
535    ///
536    /// This follows the standard Kubernetes selector pattern used by Services, `NetworkPolicies`,
537    /// and other resources for declarative resource association.
538    ///
539    /// # Example: Match podinfo records in dev/staging environments
540    ///
541    /// ```yaml
542    /// recordsFrom:
543    ///   - selector:
544    ///       matchLabels:
545    ///         app: podinfo
546    ///       matchExpressions:
547    ///         - key: environment
548    ///           operator: In
549    ///           values:
550    ///             - dev
551    ///             - staging
552    /// ```
553    ///
554    /// # Selector Operators
555    ///
556    /// - **In**: Label value must be in the specified values list
557    /// - **`NotIn`**: Label value must NOT be in the specified values list
558    /// - **Exists**: Label key must exist (any value)
559    /// - **`DoesNotExist`**: Label key must NOT exist
560    ///
561    /// # Use Cases
562    ///
563    /// - **Multi-environment zones**: Dynamically include records based on environment labels
564    /// - **Application-specific zones**: Group all records for an application using `app` label
565    /// - **Team-based zones**: Use team labels to automatically route records to team-owned zones
566    /// - **Temporary records**: Use labels to include/exclude records without changing `zoneRef`
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub records_from: Option<Vec<RecordSource>>,
569}
570
571/// `ARecord` maps a DNS name to an IPv4 address.
572///
573/// A records are the most common DNS record type, mapping hostnames to IPv4 addresses.
574/// Multiple A records can exist for the same name (round-robin DNS).
575///
576/// # Example
577///
578/// ```yaml
579/// apiVersion: bindy.firestoned.io/v1beta1
580/// kind: ARecord
581/// metadata:
582///   name: www-example-com
583///   namespace: dns-system
584///   labels:
585///     zone: example.com
586/// spec:
587///   name: www
588///   ipv4Address: 192.0.2.1
589///   ttl: 300
590/// ```
591///
592/// Records are associated with `DNSZones` via label selectors.
593/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
594#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
595#[kube(
596    group = "bindy.firestoned.io",
597    version = "v1beta1",
598    kind = "ARecord",
599    namespaced,
600    shortname = "a",
601    doc = "ARecord maps a DNS hostname to an IPv4 address. Multiple A records for the same name enable round-robin DNS load balancing.",
602    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
603    printcolumn = r#"{"name":"Address","type":"string","jsonPath":".spec.ipv4Address"}"#,
604    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
605    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
606)]
607#[kube(status = "RecordStatus")]
608#[serde(rename_all = "camelCase")]
609pub struct ARecordSpec {
610    /// Record name within the zone. Use "@" for the zone apex.
611    ///
612    /// Examples: "www", "mail", "ftp", "@"
613    /// The full DNS name will be: {name}.{zone}
614    pub name: String,
615
616    /// IPv4 address in dotted-decimal notation.
617    ///
618    /// Must be a valid IPv4 address (e.g., "192.0.2.1").
619    #[schemars(regex(
620        pattern = r"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.){3}(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)$"
621    ))]
622    pub ipv4_address: String,
623
624    /// Time To Live in seconds. Overrides zone default TTL if specified.
625    ///
626    /// Typical values: 60-86400 (1 minute to 1 day).
627    #[serde(default)]
628    #[schemars(range(min = 0, max = 2_147_483_647))]
629    pub ttl: Option<i32>,
630}
631
632/// `AAAARecord` maps a DNS name to an IPv6 address.
633///
634/// AAAA records are the IPv6 equivalent of A records, mapping hostnames to IPv6 addresses.
635///
636/// # Example
637///
638/// ```yaml
639/// apiVersion: bindy.firestoned.io/v1beta1
640/// kind: AAAARecord
641/// metadata:
642///   name: www-example-com-ipv6
643///   namespace: dns-system
644///   labels:
645///     zone: example.com
646/// spec:
647///   name: www
648///   ipv6Address: "2001:db8::1"
649///   ttl: 300
650/// ```
651///
652/// Records are associated with `DNSZones` via label selectors.
653/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
654#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
655#[kube(
656    group = "bindy.firestoned.io",
657    version = "v1beta1",
658    kind = "AAAARecord",
659    namespaced,
660    shortname = "aaaa",
661    doc = "AAAARecord maps a DNS hostname to an IPv6 address. This is the IPv6 equivalent of an A record.",
662    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
663    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
664    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
665)]
666#[kube(status = "RecordStatus")]
667#[serde(rename_all = "camelCase")]
668pub struct AAAARecordSpec {
669    /// Record name within the zone.
670    pub name: String,
671
672    /// IPv6 address in standard notation.
673    ///
674    /// Examples: `2001:db8::1`, `fe80::1`, `::1`
675    pub ipv6_address: String,
676
677    /// Time To Live in seconds.
678    #[serde(default)]
679    #[schemars(range(min = 0, max = 2_147_483_647))]
680    pub ttl: Option<i32>,
681}
682
683/// `TXTRecord` holds arbitrary text data.
684///
685/// TXT records are commonly used for SPF, DKIM, DMARC, domain verification,
686/// and other text-based metadata.
687///
688/// # Example
689///
690/// ```yaml
691/// apiVersion: bindy.firestoned.io/v1beta1
692/// kind: TXTRecord
693/// metadata:
694///   name: spf-example-com
695///   namespace: dns-system
696///   labels:
697///     zone: example.com
698/// spec:
699///   name: "@"
700///   text:
701///     - "v=spf1 include:_spf.google.com ~all"
702///   ttl: 3600
703/// ```
704///
705/// Records are associated with `DNSZones` via label selectors.
706/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
707#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
708#[kube(
709    group = "bindy.firestoned.io",
710    version = "v1beta1",
711    kind = "TXTRecord",
712    namespaced,
713    shortname = "txt",
714    doc = "TXTRecord stores arbitrary text data in DNS. Commonly used for SPF, DKIM, DMARC policies, and domain verification.",
715    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
716    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
717    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
718)]
719#[kube(status = "RecordStatus")]
720#[serde(rename_all = "camelCase")]
721pub struct TXTRecordSpec {
722    /// Record name within the zone.
723    pub name: String,
724
725    /// Array of text strings. Each string can be up to 255 characters.
726    ///
727    /// Multiple strings are concatenated by DNS resolvers.
728    /// For long text, split into multiple strings.
729    pub text: Vec<String>,
730
731    /// Time To Live in seconds.
732    #[serde(default)]
733    #[schemars(range(min = 0, max = 2_147_483_647))]
734    pub ttl: Option<i32>,
735}
736
737/// `CNAMERecord` creates an alias from one name to another.
738///
739/// CNAME (Canonical Name) records create an alias from one DNS name to another.
740/// The target can be in the same zone or a different zone.
741///
742/// **Important**: A CNAME cannot coexist with other record types for the same name.
743///
744/// # Example
745///
746/// ```yaml
747/// apiVersion: bindy.firestoned.io/v1beta1
748/// kind: CNAMERecord
749/// metadata:
750///   name: blog-example-com
751///   namespace: dns-system
752///   labels:
753///     zone: example.com
754/// spec:
755///   name: blog
756///   target: example.github.io.
757///   ttl: 3600
758/// ```
759///
760/// Records are associated with `DNSZones` via label selectors.
761/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
762#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
763#[kube(
764    group = "bindy.firestoned.io",
765    version = "v1beta1",
766    kind = "CNAMERecord",
767    namespaced,
768    shortname = "cname",
769    doc = "CNAMERecord creates a DNS alias from one hostname to another. A CNAME cannot coexist with other record types for the same name.",
770    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
771    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
772    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
773)]
774#[kube(status = "RecordStatus")]
775#[serde(rename_all = "camelCase")]
776pub struct CNAMERecordSpec {
777    /// Record name within the zone.
778    ///
779    /// Note: CNAME records cannot be created at the zone apex (@).
780    pub name: String,
781
782    /// Target hostname (canonical name).
783    ///
784    /// Should be a fully qualified domain name ending with a dot.
785    /// Example: "example.com." or "www.example.com."
786    pub target: String,
787
788    /// Time To Live in seconds.
789    #[serde(default)]
790    #[schemars(range(min = 0, max = 2_147_483_647))]
791    pub ttl: Option<i32>,
792}
793
794/// `MXRecord` specifies mail servers for a domain.
795///
796/// MX (Mail Exchange) records specify the mail servers responsible for accepting email
797/// for a domain. Lower priority values indicate higher preference.
798///
799/// # Example
800///
801/// ```yaml
802/// apiVersion: bindy.firestoned.io/v1beta1
803/// kind: MXRecord
804/// metadata:
805///   name: mail-example-com
806///   namespace: dns-system
807///   labels:
808///     zone: example.com
809/// spec:
810///   name: "@"
811///   priority: 10
812///   mailServer: mail.example.com.
813///   ttl: 3600
814/// ```
815///
816/// Records are associated with `DNSZones` via label selectors.
817/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
818#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
819#[kube(
820    group = "bindy.firestoned.io",
821    version = "v1beta1",
822    kind = "MXRecord",
823    namespaced,
824    shortname = "mx",
825    doc = "MXRecord specifies mail exchange servers for a domain. Lower priority values indicate higher preference for mail delivery.",
826    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
827    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
828    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
829)]
830#[kube(status = "RecordStatus")]
831#[serde(rename_all = "camelCase")]
832pub struct MXRecordSpec {
833    /// Record name within the zone. Use "@" for the zone apex.
834    pub name: String,
835
836    /// Priority (preference) of this mail server. Lower values = higher priority.
837    ///
838    /// Common values: 0-100. Multiple MX records can exist with different priorities.
839    #[schemars(range(min = 0, max = 65535))]
840    pub priority: i32,
841
842    /// Fully qualified domain name of the mail server.
843    ///
844    /// Must end with a dot. Example: "mail.example.com."
845    pub mail_server: String,
846
847    /// Time To Live in seconds.
848    #[serde(default)]
849    #[schemars(range(min = 0, max = 2_147_483_647))]
850    pub ttl: Option<i32>,
851}
852
853/// `NSRecord` delegates a subdomain to other nameservers.
854///
855/// NS (Nameserver) records specify which DNS servers are authoritative for a subdomain.
856/// They are used for delegating subdomains to different nameservers.
857///
858/// # Example
859///
860/// ```yaml
861/// apiVersion: bindy.firestoned.io/v1beta1
862/// kind: NSRecord
863/// metadata:
864///   name: subdomain-ns
865///   namespace: dns-system
866///   labels:
867///     zone: example.com
868/// spec:
869///   name: subdomain
870///   nameserver: ns1.other-provider.com.
871///   ttl: 86400
872/// ```
873///
874/// Records are associated with `DNSZones` via label selectors.
875/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
876#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
877#[kube(
878    group = "bindy.firestoned.io",
879    version = "v1beta1",
880    kind = "NSRecord",
881    namespaced,
882    shortname = "ns",
883    doc = "NSRecord delegates a subdomain to authoritative nameservers. Used for subdomain delegation to different DNS providers or servers.",
884    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
885    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
886    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
887)]
888#[kube(status = "RecordStatus")]
889#[serde(rename_all = "camelCase")]
890pub struct NSRecordSpec {
891    /// Subdomain to delegate. For zone apex, use "@".
892    pub name: String,
893
894    /// Fully qualified domain name of the nameserver.
895    ///
896    /// Must end with a dot. Example: "ns1.example.com."
897    pub nameserver: String,
898
899    /// Time To Live in seconds.
900    #[serde(default)]
901    #[schemars(range(min = 0, max = 2_147_483_647))]
902    pub ttl: Option<i32>,
903}
904
905/// `SRVRecord` specifies the location of services.
906///
907/// SRV (Service) records specify the hostname and port of servers for specific services.
908/// The name format is: _service._proto (e.g., _ldap._tcp, _sip._udp).
909///
910/// # Example
911///
912/// ```yaml
913/// apiVersion: bindy.firestoned.io/v1beta1
914/// kind: SRVRecord
915/// metadata:
916///   name: ldap-srv
917///   namespace: dns-system
918///   labels:
919///     zone: example.com
920/// spec:
921///   name: _ldap._tcp
922///   priority: 10
923///   weight: 60
924///   port: 389
925///   target: ldap.example.com.
926///   ttl: 3600
927/// ```
928///
929/// Records are associated with `DNSZones` via label selectors.
930/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
931#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
932#[kube(
933    group = "bindy.firestoned.io",
934    version = "v1beta1",
935    kind = "SRVRecord",
936    namespaced,
937    shortname = "srv",
938    doc = "SRVRecord specifies the hostname and port of servers for specific services. The record name follows the format _service._proto (e.g., _ldap._tcp).",
939    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
940    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
941    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
942)]
943#[kube(status = "RecordStatus")]
944#[serde(rename_all = "camelCase")]
945pub struct SRVRecordSpec {
946    /// Service and protocol in the format: _service._proto
947    ///
948    /// Example: "_ldap._tcp", "_sip._udp", "_http._tcp"
949    pub name: String,
950
951    /// Priority of the target host. Lower values = higher priority.
952    #[schemars(range(min = 0, max = 65535))]
953    pub priority: i32,
954
955    /// Relative weight for records with the same priority.
956    ///
957    /// Higher values = higher probability of selection.
958    #[schemars(range(min = 0, max = 65535))]
959    pub weight: i32,
960
961    /// TCP or UDP port where the service is available.
962    #[schemars(range(min = 0, max = 65535))]
963    pub port: i32,
964
965    /// Fully qualified domain name of the target host.
966    ///
967    /// Must end with a dot. Use "." for "service not available".
968    pub target: String,
969
970    /// Time To Live in seconds.
971    #[serde(default)]
972    #[schemars(range(min = 0, max = 2_147_483_647))]
973    pub ttl: Option<i32>,
974}
975
976/// `CAARecord` specifies Certificate Authority Authorization.
977///
978/// CAA (Certification Authority Authorization) records specify which certificate
979/// authorities are allowed to issue certificates for a domain.
980///
981/// # Example
982///
983/// ```yaml
984/// apiVersion: bindy.firestoned.io/v1beta1
985/// kind: CAARecord
986/// metadata:
987///   name: caa-letsencrypt
988///   namespace: dns-system
989///   labels:
990///     zone: example.com
991/// spec:
992///   name: "@"
993///   flags: 0
994///   tag: issue
995///   value: letsencrypt.org
996///   ttl: 86400
997/// ```
998///
999/// Records are associated with `DNSZones` via label selectors.
1000/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1001#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1002#[kube(
1003    group = "bindy.firestoned.io",
1004    version = "v1beta1",
1005    kind = "CAARecord",
1006    namespaced,
1007    shortname = "caa",
1008    doc = "CAARecord specifies which certificate authorities are authorized to issue certificates for a domain. Enhances domain security and certificate issuance control.",
1009    printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1010    printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1011    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1012)]
1013#[kube(status = "RecordStatus")]
1014#[serde(rename_all = "camelCase")]
1015pub struct CAARecordSpec {
1016    /// Record name within the zone. Use "@" for the zone apex.
1017    pub name: String,
1018
1019    /// Flags byte. Use 0 for non-critical, 128 for critical.
1020    ///
1021    /// Critical flag (128) means CAs must understand the tag.
1022    #[schemars(range(min = 0, max = 255))]
1023    pub flags: i32,
1024
1025    /// Property tag. Common values: "issue", "issuewild", "iodef".
1026    ///
1027    /// - "issue": Authorize CA to issue certificates
1028    /// - "issuewild": Authorize CA to issue wildcard certificates
1029    /// - "iodef": URL/email for violation reports
1030    pub tag: String,
1031
1032    /// Property value. Format depends on the tag.
1033    ///
1034    /// For "issue"/"issuewild": CA domain (e.g., "letsencrypt.org")
1035    /// For "iodef": mailto: or https: URL
1036    pub value: String,
1037
1038    /// Time To Live in seconds.
1039    #[serde(default)]
1040    #[schemars(range(min = 0, max = 2_147_483_647))]
1041    pub ttl: Option<i32>,
1042}
1043
1044/// Generic record status
1045#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
1046#[serde(rename_all = "camelCase")]
1047pub struct RecordStatus {
1048    #[serde(default)]
1049    pub conditions: Vec<Condition>,
1050    #[serde(skip_serializing_if = "Option::is_none")]
1051    pub observed_generation: Option<i64>,
1052    /// The FQDN of the zone that owns this record (set by `DNSZone` controller).
1053    ///
1054    /// When a `DNSZone`'s label selector matches this record, the `DNSZone` controller
1055    /// sets this field to the zone's FQDN (e.g., `"example.com"`). The record reconciler
1056    /// uses this to determine which zone to update in BIND9.
1057    ///
1058    /// If this field is empty, the record is not matched by any zone and should not
1059    /// be reconciled into BIND9.
1060    #[serde(skip_serializing_if = "Option::is_none")]
1061    pub zone: Option<String>,
1062}
1063
1064/// RNDC/TSIG algorithm for authenticated communication and zone transfers.
1065///
1066/// These HMAC algorithms are supported by BIND9 for securing RNDC communication
1067/// and zone transfers (AXFR/IXFR).
1068#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1069#[serde(rename_all = "kebab-case")]
1070pub enum RndcAlgorithm {
1071    /// HMAC-MD5 (legacy, not recommended for new deployments)
1072    HmacMd5,
1073    /// HMAC-SHA1
1074    HmacSha1,
1075    /// HMAC-SHA224
1076    HmacSha224,
1077    /// HMAC-SHA256 (recommended)
1078    #[default]
1079    HmacSha256,
1080    /// HMAC-SHA384
1081    HmacSha384,
1082    /// HMAC-SHA512
1083    HmacSha512,
1084}
1085
1086impl RndcAlgorithm {
1087    /// Convert enum to string representation expected by BIND9
1088    #[must_use]
1089    pub fn as_str(&self) -> &'static str {
1090        match self {
1091            Self::HmacMd5 => "hmac-md5",
1092            Self::HmacSha1 => "hmac-sha1",
1093            Self::HmacSha224 => "hmac-sha224",
1094            Self::HmacSha256 => "hmac-sha256",
1095            Self::HmacSha384 => "hmac-sha384",
1096            Self::HmacSha512 => "hmac-sha512",
1097        }
1098    }
1099
1100    /// Convert enum to string format expected by the rndc Rust crate.
1101    ///
1102    /// The rndc crate expects algorithm strings without the "hmac-" prefix
1103    /// (e.g., "sha256" instead of "hmac-sha256").
1104    #[must_use]
1105    pub fn as_rndc_str(&self) -> &'static str {
1106        match self {
1107            Self::HmacMd5 => "md5",
1108            Self::HmacSha1 => "sha1",
1109            Self::HmacSha224 => "sha224",
1110            Self::HmacSha256 => "sha256",
1111            Self::HmacSha384 => "sha384",
1112            Self::HmacSha512 => "sha512",
1113        }
1114    }
1115}
1116
1117/// Reference to a Kubernetes Secret containing RNDC/TSIG credentials.
1118///
1119/// This allows you to use an existing external Secret for RNDC authentication instead
1120/// of having the operator auto-generate one. The Secret is mounted as a directory at
1121/// `/etc/bind/keys/` in the BIND9 container, and BIND9 uses the `rndc.key` file.
1122///
1123/// # External (User-Managed) Secrets
1124///
1125/// For external secrets, you ONLY need to provide the `rndc.key` field containing
1126/// the complete BIND9 key file content. The other fields (`key-name`, `algorithm`,
1127/// `secret`) are optional metadata used by operator-generated secrets.
1128///
1129/// ## Minimal External Secret Example
1130///
1131/// ```yaml
1132/// apiVersion: v1
1133/// kind: Secret
1134/// metadata:
1135///   name: my-rndc-key
1136///   namespace: dns-system
1137/// type: Opaque
1138/// stringData:
1139///   rndc.key: |
1140///     key "bindy-operator" {
1141///         algorithm hmac-sha256;
1142///         secret "base64EncodedSecretKeyMaterial==";
1143///     };
1144/// ```
1145///
1146/// # Auto-Generated (Operator-Managed) Secrets
1147///
1148/// When the operator auto-generates a Secret (no `rndcSecretRef` specified), it
1149/// creates a Secret with all 4 fields for internal metadata tracking:
1150///
1151/// ```yaml
1152/// apiVersion: v1
1153/// kind: Secret
1154/// metadata:
1155///   name: bind9-instance-rndc
1156///   namespace: dns-system
1157/// type: Opaque
1158/// stringData:
1159///   key-name: "bindy-operator"     # Operator metadata
1160///   algorithm: "hmac-sha256"       # Operator metadata
1161///   secret: "randomBase64Key=="    # Operator metadata
1162///   rndc.key: |                    # Used by BIND9
1163///     key "bindy-operator" {
1164///         algorithm hmac-sha256;
1165///         secret "randomBase64Key==";
1166///     };
1167/// ```
1168///
1169/// # Using with `Bind9Instance`
1170///
1171/// ```yaml
1172/// apiVersion: bindy.firestoned.io/v1beta1
1173/// kind: Bind9Instance
1174/// metadata:
1175///   name: production-dns-primary-0
1176/// spec:
1177///   clusterRef: production-dns
1178///   role: primary
1179///   rndcSecretRef:
1180///     name: my-rndc-key
1181///     algorithm: hmac-sha256
1182/// ```
1183///
1184/// # How It Works
1185///
1186/// When the Secret is mounted at `/etc/bind/keys/`, Kubernetes creates individual
1187/// files for each Secret key:
1188/// - `/etc/bind/keys/rndc.key` (the BIND9 key file) ← **This is what BIND9 uses**
1189/// - `/etc/bind/keys/key-name` (optional metadata for operator-generated secrets)
1190/// - `/etc/bind/keys/algorithm` (optional metadata for operator-generated secrets)
1191/// - `/etc/bind/keys/secret` (optional metadata for operator-generated secrets)
1192///
1193/// The `rndc.conf` file includes `/etc/bind/keys/rndc.key`, so BIND9 only needs
1194/// that one file to exist
1195#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1196#[serde(rename_all = "camelCase")]
1197pub struct RndcSecretRef {
1198    /// Name of the Kubernetes Secret containing RNDC credentials
1199    pub name: String,
1200
1201    /// HMAC algorithm for this key
1202    #[serde(default)]
1203    pub algorithm: RndcAlgorithm,
1204
1205    /// Key within the secret for the key name (default: "key-name")
1206    #[serde(default = "default_key_name_key")]
1207    pub key_name_key: String,
1208
1209    /// Key within the secret for the secret value (default: "secret")
1210    #[serde(default = "default_secret_key")]
1211    pub secret_key: String,
1212}
1213
1214fn default_key_name_key() -> String {
1215    "key-name".to_string()
1216}
1217
1218fn default_secret_key() -> String {
1219    "secret".to_string()
1220}
1221
1222/// Default BIND9 version for clusters when not specified
1223#[allow(clippy::unnecessary_wraps)]
1224fn default_bind9_version() -> Option<String> {
1225    Some(crate::constants::DEFAULT_BIND9_VERSION.to_string())
1226}
1227
1228/// TSIG Key configuration for authenticated zone transfers (deprecated in favor of `RndcSecretRef`)
1229#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1230#[serde(rename_all = "camelCase")]
1231pub struct TSIGKey {
1232    /// Name of the TSIG key
1233    pub name: String,
1234    /// Algorithm for HMAC-based authentication
1235    pub algorithm: RndcAlgorithm,
1236    /// Secret key (base64 encoded) - should reference a Secret
1237    pub secret: String,
1238}
1239
1240/// BIND9 server configuration options
1241///
1242/// These settings configure the BIND9 DNS server behavior including recursion,
1243/// access control lists, DNSSEC, and network listeners.
1244#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1245#[serde(rename_all = "camelCase")]
1246pub struct Bind9Config {
1247    /// Enable or disable recursive DNS queries
1248    ///
1249    /// When enabled (`true`), the DNS server will recursively resolve queries by
1250    /// contacting other authoritative nameservers. When disabled (`false`), the
1251    /// server only answers for zones it is authoritative for.
1252    ///
1253    /// Default: `false` (authoritative-only mode)
1254    ///
1255    /// **Important**: Recursive resolvers should not be publicly accessible due to
1256    /// security risks (DNS amplification attacks, cache poisoning).
1257    #[serde(default)]
1258    pub recursion: Option<bool>,
1259
1260    /// Access control list for DNS queries
1261    ///
1262    /// Specifies which IP addresses or networks are allowed to query this DNS server.
1263    /// Supports CIDR notation and special keywords.
1264    ///
1265    /// Default: Not set (BIND9 defaults to localhost only)
1266    ///
1267    /// Examples:
1268    /// - `["0.0.0.0/0"]` - Allow queries from any IPv4 address
1269    /// - `["10.0.0.0/8", "172.16.0.0/12"]` - Allow queries from private networks
1270    /// - `["any"]` - Allow queries from any IP (IPv4 and IPv6)
1271    /// - `["none"]` - Deny all queries
1272    /// - `["localhost"]` - Allow only from localhost
1273    #[serde(default)]
1274    pub allow_query: Option<Vec<String>>,
1275
1276    /// Access control list for zone transfers (AXFR/IXFR)
1277    ///
1278    /// Specifies which IP addresses or networks are allowed to perform zone transfers
1279    /// from this server. Zone transfers are used for replication between primary and
1280    /// secondary DNS servers.
1281    ///
1282    /// Default: Auto-detected cluster Pod CIDRs (e.g., `["10.42.0.0/16"]`)
1283    ///
1284    /// Examples:
1285    /// - `["10.42.0.0/16"]` - Allow transfers from specific Pod network
1286    /// - `["10.0.0.0/8"]` - Allow transfers from entire private network
1287    /// - `[]` - Deny all zone transfers (empty list means "none")
1288    /// - `["any"]` - Allow transfers from any IP (not recommended for production)
1289    ///
1290    /// Can be overridden at cluster level via `spec.primary.allowTransfer` or
1291    /// `spec.secondary.allowTransfer` for role-specific ACLs.
1292    #[serde(default)]
1293    pub allow_transfer: Option<Vec<String>>,
1294
1295    /// DNSSEC (DNS Security Extensions) configuration
1296    ///
1297    /// Configures DNSSEC signing and validation. DNSSEC provides cryptographic
1298    /// authentication of DNS data to prevent spoofing and cache poisoning attacks.
1299    ///
1300    /// See `DNSSECConfig` for detailed options.
1301    #[serde(default)]
1302    pub dnssec: Option<DNSSECConfig>,
1303
1304    /// DNS forwarders for recursive resolution
1305    ///
1306    /// List of upstream DNS servers to forward queries to when recursion is enabled.
1307    /// Used for hybrid authoritative/recursive configurations.
1308    ///
1309    /// Only relevant when `recursion: true`.
1310    ///
1311    /// Examples:
1312    /// - `["8.8.8.8", "8.8.4.4"]` - Google Public DNS
1313    /// - `["1.1.1.1", "1.0.0.1"]` - Cloudflare DNS
1314    /// - `["10.0.0.53"]` - Internal corporate DNS resolver
1315    #[serde(default)]
1316    pub forwarders: Option<Vec<String>>,
1317
1318    /// IPv4 addresses to listen on for DNS queries
1319    ///
1320    /// Specifies which IPv4 interfaces and ports the DNS server should bind to.
1321    ///
1322    /// Default: All IPv4 interfaces on port 53
1323    ///
1324    /// Examples:
1325    /// - `["any"]` - Listen on all IPv4 interfaces
1326    /// - `["127.0.0.1"]` - Listen only on localhost
1327    /// - `["10.0.0.1"]` - Listen on specific IP address
1328    #[serde(default)]
1329    pub listen_on: Option<Vec<String>>,
1330
1331    /// IPv6 addresses to listen on for DNS queries
1332    ///
1333    /// Specifies which IPv6 interfaces and ports the DNS server should bind to.
1334    ///
1335    /// Default: All IPv6 interfaces on port 53 (if IPv6 is available)
1336    ///
1337    /// Examples:
1338    /// - `["any"]` - Listen on all IPv6 interfaces
1339    /// - `["::1"]` - Listen only on IPv6 localhost
1340    /// - `["none"]` - Disable IPv6 listening
1341    #[serde(default)]
1342    pub listen_on_v6: Option<Vec<String>>,
1343
1344    /// Reference to an existing Kubernetes Secret containing RNDC key.
1345    ///
1346    /// If specified at the global config level, all instances in the cluster will use
1347    /// this existing Secret instead of auto-generating individual secrets, unless
1348    /// overridden at the role (primary/secondary) or instance level.
1349    ///
1350    /// This allows centralized RNDC key management for the entire cluster.
1351    ///
1352    /// Precedence order (highest to lowest):
1353    /// 1. Instance level (`spec.rndcSecretRef`)
1354    /// 2. Role level (`spec.primary.rndcSecretRef` or `spec.secondary.rndcSecretRef`)
1355    /// 3. Global level (`spec.global.rndcSecretRef`)
1356    /// 4. Auto-generated (default)
1357    #[serde(default)]
1358    pub rndc_secret_ref: Option<RndcSecretRef>,
1359
1360    /// Bindcar RNDC API sidecar container configuration.
1361    ///
1362    /// The API container provides an HTTP interface for managing zones via rndc.
1363    /// This configuration is inherited by all instances unless overridden.
1364    #[serde(default)]
1365    pub bindcar_config: Option<BindcarConfig>,
1366}
1367
1368/// DNSSEC (DNS Security Extensions) configuration
1369///
1370/// DNSSEC adds cryptographic signatures to DNS records to ensure authenticity and integrity.
1371#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1372#[serde(rename_all = "camelCase")]
1373pub struct DNSSECConfig {
1374    /// Enable DNSSEC validation of responses
1375    ///
1376    /// When enabled, BIND will validate DNSSEC signatures on responses from other
1377    /// nameservers. Invalid or missing signatures will cause queries to fail.
1378    ///
1379    /// Default: `false`
1380    ///
1381    /// **Important**: Requires valid DNSSEC trust anchors and proper network connectivity
1382    /// to root DNS servers. May cause resolution failures if DNSSEC is broken upstream.
1383    #[serde(default)]
1384    pub validation: Option<bool>,
1385}
1386
1387/// Container image configuration for BIND9 instances
1388#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1389#[serde(rename_all = "camelCase")]
1390pub struct ImageConfig {
1391    /// Container image repository and tag for BIND9
1392    ///
1393    /// Example: "internetsystemsconsortium/bind9:9.18"
1394    #[serde(default)]
1395    pub image: Option<String>,
1396
1397    /// Image pull policy
1398    ///
1399    /// Example: `IfNotPresent`, `Always`, `Never`
1400    #[serde(default)]
1401    pub image_pull_policy: Option<String>,
1402
1403    /// Reference to image pull secrets for private registries
1404    #[serde(default)]
1405    pub image_pull_secrets: Option<Vec<String>>,
1406}
1407
1408/// `ConfigMap` references for BIND9 configuration files
1409#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1410#[serde(rename_all = "camelCase")]
1411pub struct ConfigMapRefs {
1412    /// `ConfigMap` containing named.conf file
1413    ///
1414    /// If not specified, a default configuration will be generated
1415    #[serde(default)]
1416    pub named_conf: Option<String>,
1417
1418    /// `ConfigMap` containing named.conf.options file
1419    ///
1420    /// If not specified, a default configuration will be generated
1421    #[serde(default)]
1422    pub named_conf_options: Option<String>,
1423
1424    /// `ConfigMap` containing named.conf.zones file
1425    ///
1426    /// Optional. If specified, the zones file from this `ConfigMap` will be included in named.conf.
1427    /// If not specified, no zones file will be included (zones can be added dynamically via RNDC).
1428    /// Use this for pre-configured zones or to import existing BIND9 zone configurations.
1429    #[serde(default)]
1430    pub named_conf_zones: Option<String>,
1431}
1432
1433/// Service configuration including spec and annotations
1434///
1435/// Allows customization of both the Kubernetes Service spec and metadata annotations.
1436#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default)]
1437#[serde(rename_all = "camelCase")]
1438pub struct ServiceConfig {
1439    /// Annotations to apply to the Service metadata
1440    ///
1441    /// Common use cases:
1442    /// - `MetalLB` address pool selection: `metallb.universe.tf/address-pool: my-ip-pool`
1443    /// - AWS load balancer configuration: `service.beta.kubernetes.io/aws-load-balancer-type: nlb`
1444    /// - External DNS hostname: `external-dns.alpha.kubernetes.io/hostname: dns.example.com`
1445    ///
1446    /// Example:
1447    /// ```yaml
1448    /// annotations:
1449    ///   metallb.universe.tf/address-pool: my-ip-pool
1450    ///   external-dns.alpha.kubernetes.io/hostname: ns1.example.com
1451    /// ```
1452    #[serde(skip_serializing_if = "Option::is_none")]
1453    pub annotations: Option<BTreeMap<String, String>>,
1454
1455    /// Custom Kubernetes Service spec
1456    ///
1457    /// Allows full customization of the Kubernetes Service created for DNS servers.
1458    /// This accepts the same fields as the standard Kubernetes Service `spec`.
1459    ///
1460    /// Common fields:
1461    /// - `type`: Service type (`ClusterIP`, `NodePort`, `LoadBalancer`)
1462    /// - `loadBalancerIP`: Specific IP for `LoadBalancer` type
1463    /// - `externalTrafficPolicy`: `Local` or `Cluster`
1464    /// - `sessionAffinity`: `ClientIP` or `None`
1465    /// - `clusterIP`: Specific cluster IP (use with caution)
1466    ///
1467    /// Fields specified here are merged with defaults. Unspecified fields use safe defaults:
1468    /// - `type: ClusterIP` (if not specified)
1469    /// - Ports 53/TCP and 53/UDP (always set)
1470    /// - Selector matching the instance labels (always set)
1471    #[serde(skip_serializing_if = "Option::is_none")]
1472    pub spec: Option<ServiceSpec>,
1473}
1474
1475/// Primary instance configuration
1476///
1477/// Groups all configuration specific to primary (authoritative) DNS instances.
1478#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default)]
1479#[serde(rename_all = "camelCase")]
1480pub struct PrimaryConfig {
1481    /// Number of primary instance replicas (default: 1)
1482    ///
1483    /// This controls how many replicas each primary instance in this cluster should have.
1484    /// Can be overridden at the instance level.
1485    #[serde(skip_serializing_if = "Option::is_none")]
1486    #[schemars(range(min = 0, max = 100))]
1487    pub replicas: Option<i32>,
1488
1489    /// Custom Kubernetes Service configuration for primary instances
1490    ///
1491    /// Allows full customization of the Kubernetes Service created for primary DNS servers,
1492    /// including both Service spec fields and metadata annotations.
1493    ///
1494    /// Annotations are commonly used for:
1495    /// - `MetalLB` address pool selection
1496    /// - Cloud provider load balancer configuration
1497    /// - External DNS integration
1498    /// - Linkerd service mesh annotations
1499    ///
1500    /// Fields specified here are merged with defaults. Unspecified fields use safe defaults:
1501    /// - `type: ClusterIP` (if not specified)
1502    /// - Ports 53/TCP and 53/UDP (always set)
1503    /// - Selector matching the instance labels (always set)
1504    #[serde(skip_serializing_if = "Option::is_none")]
1505    pub service: Option<ServiceConfig>,
1506
1507    /// Allow-transfer ACL for primary instances
1508    ///
1509    /// Overrides the default auto-detected Pod CIDR allow-transfer configuration
1510    /// for all primary instances in this cluster. Use this to restrict or expand
1511    /// which IP addresses can perform zone transfers from primary servers.
1512    ///
1513    /// If not specified, defaults to cluster Pod CIDRs (auto-detected from Kubernetes Nodes).
1514    ///
1515    /// Examples:
1516    /// - `["10.0.0.0/8"]` - Allow transfers from entire 10.x network
1517    /// - `["any"]` - Allow transfers from any IP (public internet)
1518    /// - `[]` - Deny all zone transfers (empty list means "none")
1519    ///
1520    /// Can be overridden at the instance level via `spec.config.allowTransfer`.
1521    #[serde(default, skip_serializing_if = "Option::is_none")]
1522    pub allow_transfer: Option<Vec<String>>,
1523
1524    /// Reference to an existing Kubernetes Secret containing RNDC key for all primary instances.
1525    ///
1526    /// If specified, all primary instances in this cluster will use this existing Secret
1527    /// instead of auto-generating individual secrets. This allows sharing the same RNDC key
1528    /// across all primary instances.
1529    ///
1530    /// Can be overridden at the instance level via `spec.rndcSecretRef`.
1531    #[serde(default, skip_serializing_if = "Option::is_none")]
1532    pub rndc_secret_ref: Option<RndcSecretRef>,
1533}
1534
1535/// Secondary instance configuration
1536///
1537/// Groups all configuration specific to secondary (replica) DNS instances.
1538#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default)]
1539#[serde(rename_all = "camelCase")]
1540pub struct SecondaryConfig {
1541    /// Number of secondary instance replicas (default: 1)
1542    ///
1543    /// This controls how many replicas each secondary instance in this cluster should have.
1544    /// Can be overridden at the instance level.
1545    #[serde(skip_serializing_if = "Option::is_none")]
1546    #[schemars(range(min = 0, max = 100))]
1547    pub replicas: Option<i32>,
1548
1549    /// Custom Kubernetes Service configuration for secondary instances
1550    ///
1551    /// Allows full customization of the Kubernetes Service created for secondary DNS servers,
1552    /// including both Service spec fields and metadata annotations.
1553    ///
1554    /// Annotations are commonly used for:
1555    /// - `MetalLB` address pool selection
1556    /// - Cloud provider load balancer configuration
1557    /// - External DNS integration
1558    /// - Linkerd service mesh annotations
1559    ///
1560    /// Allows different service configurations for primary vs secondary instances.
1561    /// Example: Primaries use `LoadBalancer` with specific annotations, secondaries use `ClusterIP`
1562    ///
1563    /// See `PrimaryConfig.service` for detailed field documentation.
1564    #[serde(skip_serializing_if = "Option::is_none")]
1565    pub service: Option<ServiceConfig>,
1566
1567    /// Allow-transfer ACL for secondary instances
1568    ///
1569    /// Overrides the default auto-detected Pod CIDR allow-transfer configuration
1570    /// for all secondary instances in this cluster. Use this to restrict or expand
1571    /// which IP addresses can perform zone transfers from secondary servers.
1572    ///
1573    /// If not specified, defaults to cluster Pod CIDRs (auto-detected from Kubernetes Nodes).
1574    ///
1575    /// Examples:
1576    /// - `["10.0.0.0/8"]` - Allow transfers from entire 10.x network
1577    /// - `["any"]` - Allow transfers from any IP (public internet)
1578    /// - `[]` - Deny all zone transfers (empty list means "none")
1579    ///
1580    /// Can be overridden at the instance level via `spec.config.allowTransfer`.
1581    #[serde(default, skip_serializing_if = "Option::is_none")]
1582    pub allow_transfer: Option<Vec<String>>,
1583
1584    /// Reference to an existing Kubernetes Secret containing RNDC key for all secondary instances.
1585    ///
1586    /// If specified, all secondary instances in this cluster will use this existing Secret
1587    /// instead of auto-generating individual secrets. This allows sharing the same RNDC key
1588    /// across all secondary instances.
1589    ///
1590    /// Can be overridden at the instance level via `spec.rndcSecretRef`.
1591    #[serde(default, skip_serializing_if = "Option::is_none")]
1592    pub rndc_secret_ref: Option<RndcSecretRef>,
1593}
1594
1595/// Common specification fields shared between namespace-scoped and cluster-scoped BIND9 clusters.
1596///
1597/// This struct contains all configuration that is common to both `Bind9Cluster` (namespace-scoped)
1598/// and `ClusterBind9Provider` (cluster-scoped). By using this shared struct, we avoid code duplication
1599/// and ensure consistency between the two cluster types.
1600#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1601#[serde(rename_all = "camelCase")]
1602pub struct Bind9ClusterCommonSpec {
1603    /// Shared BIND9 version for the cluster
1604    ///
1605    /// If not specified, defaults to "9.18".
1606    #[serde(default = "default_bind9_version")]
1607    #[schemars(default = "default_bind9_version")]
1608    pub version: Option<String>,
1609
1610    /// Primary instance configuration
1611    ///
1612    /// Configuration specific to primary (authoritative) DNS instances,
1613    /// including replica count and service specifications.
1614    #[serde(default, skip_serializing_if = "Option::is_none")]
1615    pub primary: Option<PrimaryConfig>,
1616
1617    /// Secondary instance configuration
1618    ///
1619    /// Configuration specific to secondary (replica) DNS instances,
1620    /// including replica count and service specifications.
1621    #[serde(default, skip_serializing_if = "Option::is_none")]
1622    pub secondary: Option<SecondaryConfig>,
1623
1624    /// Container image configuration
1625    #[serde(default)]
1626    pub image: Option<ImageConfig>,
1627
1628    /// `ConfigMap` references for BIND9 configuration files
1629    #[serde(default)]
1630    pub config_map_refs: Option<ConfigMapRefs>,
1631
1632    /// Global configuration shared by all instances in the cluster
1633    ///
1634    /// This configuration applies to all instances (both primary and secondary)
1635    /// unless overridden at the instance level or by role-specific configuration.
1636    #[serde(default)]
1637    pub global: Option<Bind9Config>,
1638
1639    /// References to Kubernetes Secrets containing RNDC/TSIG keys for authenticated zone transfers.
1640    ///
1641    /// Each secret should contain the key name, algorithm, and base64-encoded secret value.
1642    /// These secrets are used for secure communication with BIND9 instances via RNDC and
1643    /// for authenticated zone transfers (AXFR/IXFR) between primary and secondary servers.
1644    #[serde(default)]
1645    pub rndc_secret_refs: Option<Vec<RndcSecretRef>>,
1646
1647    /// ACLs that can be referenced by instances
1648    #[serde(default)]
1649    pub acls: Option<BTreeMap<String, Vec<String>>>,
1650
1651    /// Volumes that can be mounted by instances in this cluster
1652    ///
1653    /// These volumes are inherited by all instances unless overridden.
1654    /// Common use cases include `PersistentVolumeClaims` for zone data storage.
1655    #[serde(default)]
1656    pub volumes: Option<Vec<Volume>>,
1657
1658    /// Volume mounts that specify where volumes should be mounted in containers
1659    ///
1660    /// These mounts are inherited by all instances unless overridden.
1661    #[serde(default)]
1662    pub volume_mounts: Option<Vec<VolumeMount>>,
1663}
1664
1665/// `Bind9Cluster` - Namespace-scoped DNS cluster for tenant-managed infrastructure.
1666///
1667/// A namespace-scoped cluster allows development teams to run their own isolated BIND9
1668/// DNS infrastructure within their namespace. Each team can manage their own cluster
1669/// independently, with RBAC controlling who can create and manage resources.
1670///
1671/// For platform-managed, cluster-wide DNS infrastructure, use `ClusterBind9Provider` instead.
1672///
1673/// # Use Cases
1674///
1675/// - Development teams need isolated DNS infrastructure for testing
1676/// - Multi-tenant environments where each team manages their own DNS
1677/// - Namespaced DNS services that don't need cluster-wide visibility
1678///
1679/// # Example
1680///
1681/// ```yaml
1682/// apiVersion: bindy.firestoned.io/v1beta1
1683/// kind: Bind9Cluster
1684/// metadata:
1685///   name: dev-team-dns
1686///   namespace: dev-team-alpha
1687/// spec:
1688///   version: "9.18"
1689///   primary:
1690///     replicas: 1
1691///   secondary:
1692///     replicas: 1
1693/// ```
1694#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1695#[kube(
1696    group = "bindy.firestoned.io",
1697    version = "v1beta1",
1698    kind = "Bind9Cluster",
1699    namespaced,
1700    shortname = "b9c",
1701    shortname = "b9cs",
1702    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.",
1703    printcolumn = r#"{"name":"Version","type":"string","jsonPath":".spec.version"}"#,
1704    printcolumn = r#"{"name":"Primary","type":"integer","jsonPath":".spec.primary.replicas"}"#,
1705    printcolumn = r#"{"name":"Secondary","type":"integer","jsonPath":".spec.secondary.replicas"}"#,
1706    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1707)]
1708#[kube(status = "Bind9ClusterStatus")]
1709#[serde(rename_all = "camelCase")]
1710pub struct Bind9ClusterSpec {
1711    /// All cluster configuration is flattened from the common spec
1712    #[serde(flatten)]
1713    pub common: Bind9ClusterCommonSpec,
1714}
1715
1716/// `ClusterBind9Provider` - Cluster-scoped BIND9 DNS provider for platform teams.
1717///
1718/// A cluster-scoped provider allows platform teams to provision shared BIND9 DNS infrastructure
1719/// that is accessible from any namespace. This is ideal for shared services, production DNS,
1720/// or platform-managed infrastructure that multiple teams use.
1721///
1722/// `DNSZones` in any namespace can reference a `ClusterBind9Provider` using the `clusterProviderRef` field.
1723///
1724/// # Use Cases
1725///
1726/// - Platform team provides shared DNS infrastructure for all namespaces
1727/// - Production DNS services that serve multiple applications
1728/// - Centrally managed DNS with governance and compliance requirements
1729///
1730/// # Example
1731///
1732/// ```yaml
1733/// apiVersion: bindy.firestoned.io/v1beta1
1734/// kind: ClusterBind9Provider
1735/// metadata:
1736///   name: shared-production-dns
1737///   # No namespace - cluster-scoped
1738/// spec:
1739///   version: "9.18"
1740///   primary:
1741///     replicas: 3
1742///     service:
1743///       type: LoadBalancer
1744///   secondary:
1745///     replicas: 2
1746/// ```
1747#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1748#[kube(
1749    group = "bindy.firestoned.io",
1750    version = "v1beta1",
1751    kind = "ClusterBind9Provider",
1752    // NOTE: No 'namespaced' attribute = cluster-scoped
1753    shortname = "cb9p",
1754    shortname = "cb9ps",
1755    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.",
1756    printcolumn = r#"{"name":"Version","type":"string","jsonPath":".spec.version"}"#,
1757    printcolumn = r#"{"name":"Primary","type":"integer","jsonPath":".spec.primary.replicas"}"#,
1758    printcolumn = r#"{"name":"Secondary","type":"integer","jsonPath":".spec.secondary.replicas"}"#,
1759    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1760)]
1761#[kube(status = "Bind9ClusterStatus")]
1762#[serde(rename_all = "camelCase")]
1763pub struct ClusterBind9ProviderSpec {
1764    /// Namespace where `Bind9Instance` resources will be created
1765    ///
1766    /// Since `ClusterBind9Provider` is cluster-scoped, instances need to be created in a specific namespace.
1767    /// Typically this would be a platform-managed namespace like `dns-system`.
1768    ///
1769    /// All managed instances (primary and secondary) will be created in this namespace.
1770    /// `DNSZones` from any namespace can reference this provider via `clusterProviderRef`.
1771    ///
1772    /// **Default:** If not specified, instances will be created in the same namespace where the
1773    /// Bindy operator is running (from `POD_NAMESPACE` environment variable).
1774    ///
1775    /// Example: `dns-system` for platform DNS infrastructure
1776    #[serde(skip_serializing_if = "Option::is_none")]
1777    pub namespace: Option<String>,
1778
1779    /// All cluster configuration is flattened from the common spec
1780    #[serde(flatten)]
1781    pub common: Bind9ClusterCommonSpec,
1782}
1783
1784/// `Bind9Cluster` status
1785#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
1786#[serde(rename_all = "camelCase")]
1787pub struct Bind9ClusterStatus {
1788    /// Status conditions for this cluster
1789    #[serde(default)]
1790    pub conditions: Vec<Condition>,
1791
1792    /// Observed generation for optimistic concurrency
1793    #[serde(skip_serializing_if = "Option::is_none")]
1794    pub observed_generation: Option<i64>,
1795
1796    /// Number of instances in this cluster
1797    #[serde(skip_serializing_if = "Option::is_none")]
1798    pub instance_count: Option<i32>,
1799
1800    /// Number of ready instances
1801    #[serde(skip_serializing_if = "Option::is_none")]
1802    pub ready_instances: Option<i32>,
1803
1804    /// Names of `Bind9Instance` resources created for this cluster
1805    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1806    pub instances: Vec<String>,
1807}
1808
1809/// Server role in the DNS cluster.
1810///
1811/// Determines whether the instance is authoritative (primary) or replicates from primaries (secondary).
1812#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1813#[serde(rename_all = "lowercase")]
1814pub enum ServerRole {
1815    /// Primary DNS server - authoritative source for zones.
1816    ///
1817    /// Primary servers hold the original zone data and process dynamic updates.
1818    /// Changes to zones are made on primaries and transferred to secondaries.
1819    Primary,
1820
1821    /// Secondary DNS server - replicates zones from primary servers.
1822    ///
1823    /// Secondary servers receive zone data via AXFR (full) or IXFR (incremental)
1824    /// zone transfers. They provide redundancy and geographic distribution.
1825    Secondary,
1826}
1827
1828/// `Bind9Instance` represents a BIND9 DNS server deployment in Kubernetes.
1829///
1830/// Each `Bind9Instance` creates a Deployment, Service, `ConfigMap`, and Secret for managing
1831/// a BIND9 server. The instance communicates with the controller via RNDC protocol.
1832///
1833/// # Example
1834///
1835/// ```yaml
1836/// apiVersion: bindy.firestoned.io/v1beta1
1837/// kind: Bind9Instance
1838/// metadata:
1839///   name: dns-primary
1840///   namespace: dns-system
1841/// spec:
1842///   clusterRef: my-dns-cluster
1843///   role: primary
1844///   replicas: 2
1845///   version: "9.18"
1846/// ```
1847#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1848#[kube(
1849    group = "bindy.firestoned.io",
1850    version = "v1beta1",
1851    kind = "Bind9Instance",
1852    namespaced,
1853    shortname = "b9",
1854    shortname = "b9s",
1855    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.",
1856    printcolumn = r#"{"name":"Cluster","type":"string","jsonPath":".spec.clusterRef"}"#,
1857    printcolumn = r#"{"name":"Role","type":"string","jsonPath":".spec.role"}"#,
1858    printcolumn = r#"{"name":"Replicas","type":"integer","jsonPath":".spec.replicas"}"#,
1859    printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1860)]
1861#[kube(status = "Bind9InstanceStatus")]
1862#[serde(rename_all = "camelCase")]
1863pub struct Bind9InstanceSpec {
1864    /// Reference to the cluster this instance belongs to.
1865    ///
1866    /// Can reference either:
1867    /// - A namespace-scoped `Bind9Cluster` (must be in the same namespace as this instance)
1868    /// - A cluster-scoped `ClusterBind9Provider` (cluster-wide, accessible from any namespace)
1869    ///
1870    /// The cluster provides shared configuration and defines the logical grouping.
1871    /// The controller will automatically detect whether this references a namespace-scoped
1872    /// or cluster-scoped cluster resource.
1873    pub cluster_ref: String,
1874
1875    /// Role of this instance (primary or secondary).
1876    ///
1877    /// Primary instances are authoritative for zones. Secondary instances
1878    /// replicate zones from primaries via AXFR/IXFR.
1879    pub role: ServerRole,
1880
1881    /// Number of pod replicas for high availability.
1882    ///
1883    /// Defaults to 1 if not specified. For production, use 2+ replicas.
1884    #[serde(default)]
1885    #[schemars(range(min = 0, max = 100))]
1886    pub replicas: Option<i32>,
1887
1888    /// BIND9 version override. Inherits from cluster if not specified.
1889    ///
1890    /// Example: "9.18", "9.16"
1891    #[serde(default)]
1892    pub version: Option<String>,
1893
1894    /// Container image configuration override. Inherits from cluster if not specified.
1895    #[serde(default)]
1896    pub image: Option<ImageConfig>,
1897
1898    /// `ConfigMap` references override. Inherits from cluster if not specified.
1899    #[serde(default)]
1900    pub config_map_refs: Option<ConfigMapRefs>,
1901
1902    /// Instance-specific BIND9 configuration overrides.
1903    ///
1904    /// Overrides cluster-level configuration for this instance only.
1905    #[serde(default)]
1906    pub config: Option<Bind9Config>,
1907
1908    /// Primary server addresses for zone transfers (required for secondary instances).
1909    ///
1910    /// List of IP addresses or hostnames of primary servers to transfer zones from.
1911    /// Example: `["10.0.1.10", "primary.example.com"]`
1912    #[serde(default)]
1913    pub primary_servers: Option<Vec<String>>,
1914
1915    /// Volumes override for this instance. Inherits from cluster if not specified.
1916    ///
1917    /// These volumes override cluster-level volumes. Common use cases include
1918    /// instance-specific `PersistentVolumeClaims` for zone data storage.
1919    #[serde(default)]
1920    pub volumes: Option<Vec<Volume>>,
1921
1922    /// Volume mounts override for this instance. Inherits from cluster if not specified.
1923    ///
1924    /// These mounts override cluster-level volume mounts.
1925    #[serde(default)]
1926    pub volume_mounts: Option<Vec<VolumeMount>>,
1927
1928    /// Reference to an existing Kubernetes Secret containing RNDC key.
1929    ///
1930    /// If specified, uses this existing Secret instead of auto-generating one.
1931    /// The Secret must contain the keys specified in the reference (defaults: "key-name", "algorithm", "secret", "rndc.key").
1932    /// This allows sharing RNDC keys across instances or using externally managed secrets.
1933    ///
1934    /// If not specified, a Secret will be auto-generated for this instance.
1935    #[serde(default)]
1936    pub rndc_secret_ref: Option<RndcSecretRef>,
1937
1938    /// Storage configuration for zone files.
1939    ///
1940    /// Specifies how zone files should be stored. Defaults to emptyDir (ephemeral storage).
1941    /// For persistent storage, use persistentVolumeClaim.
1942    #[serde(default)]
1943    pub storage: Option<StorageConfig>,
1944
1945    /// Bindcar RNDC API sidecar container configuration.
1946    ///
1947    /// The API container provides an HTTP interface for managing zones via rndc.
1948    /// If not specified, uses default configuration.
1949    #[serde(default)]
1950    pub bindcar_config: Option<BindcarConfig>,
1951}
1952
1953/// `Bind9Instance` status
1954#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
1955#[serde(rename_all = "camelCase")]
1956pub struct Bind9InstanceStatus {
1957    #[serde(default)]
1958    pub conditions: Vec<Condition>,
1959    #[serde(skip_serializing_if = "Option::is_none")]
1960    pub observed_generation: Option<i64>,
1961    #[serde(skip_serializing_if = "Option::is_none")]
1962    pub replicas: Option<i32>,
1963    #[serde(skip_serializing_if = "Option::is_none")]
1964    pub ready_replicas: Option<i32>,
1965    /// IP or hostname of this instance's service
1966    #[serde(skip_serializing_if = "Option::is_none")]
1967    pub service_address: Option<String>,
1968}
1969
1970/// Storage configuration for zone files
1971#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1972#[serde(rename_all = "camelCase")]
1973pub struct StorageConfig {
1974    /// Storage type (emptyDir or persistentVolumeClaim)
1975    #[serde(default = "default_storage_type")]
1976    pub storage_type: StorageType,
1977
1978    /// `EmptyDir` configuration (used when storageType is emptyDir)
1979    #[serde(skip_serializing_if = "Option::is_none")]
1980    pub empty_dir: Option<k8s_openapi::api::core::v1::EmptyDirVolumeSource>,
1981
1982    /// `PersistentVolumeClaim` configuration (used when storageType is persistentVolumeClaim)
1983    #[serde(skip_serializing_if = "Option::is_none")]
1984    pub persistent_volume_claim: Option<PersistentVolumeClaimConfig>,
1985}
1986
1987fn default_storage_type() -> StorageType {
1988    StorageType::EmptyDir
1989}
1990
1991/// Storage type for zone files
1992#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
1993#[serde(rename_all = "camelCase")]
1994pub enum StorageType {
1995    /// Ephemeral storage (default) - data is lost when pod restarts
1996    EmptyDir,
1997    /// Persistent storage - data survives pod restarts
1998    PersistentVolumeClaim,
1999}
2000
2001/// `PersistentVolumeClaim` configuration
2002#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2003#[serde(rename_all = "camelCase")]
2004pub struct PersistentVolumeClaimConfig {
2005    /// Name of an existing PVC to use
2006    #[serde(skip_serializing_if = "Option::is_none")]
2007    pub claim_name: Option<String>,
2008
2009    /// Storage class name for dynamic provisioning
2010    #[serde(skip_serializing_if = "Option::is_none")]
2011    pub storage_class_name: Option<String>,
2012
2013    /// Storage size (e.g., "10Gi", "1Ti")
2014    #[serde(skip_serializing_if = "Option::is_none")]
2015    pub size: Option<String>,
2016
2017    /// Access modes (`ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`)
2018    #[serde(skip_serializing_if = "Option::is_none")]
2019    pub access_modes: Option<Vec<String>>,
2020}
2021
2022/// Bindcar container configuration
2023#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2024#[serde(rename_all = "camelCase")]
2025pub struct BindcarConfig {
2026    /// Container image for the RNDC API sidecar
2027    ///
2028    /// Example: "ghcr.io/firestoned/bindcar:v0.3.0"
2029    #[serde(skip_serializing_if = "Option::is_none")]
2030    pub image: Option<String>,
2031
2032    /// Image pull policy (`Always`, `IfNotPresent`, `Never`)
2033    #[serde(skip_serializing_if = "Option::is_none")]
2034    pub image_pull_policy: Option<String>,
2035
2036    /// Resource requirements for the Bindcar container
2037    #[serde(skip_serializing_if = "Option::is_none")]
2038    pub resources: Option<k8s_openapi::api::core::v1::ResourceRequirements>,
2039
2040    /// API server port (default: 8080)
2041    #[serde(skip_serializing_if = "Option::is_none")]
2042    pub port: Option<i32>,
2043
2044    /// Log level for the Bindcar container (`debug`, `info`, `warn`, `error`)
2045    #[serde(skip_serializing_if = "Option::is_none")]
2046    pub log_level: Option<String>,
2047
2048    /// Environment variables for the Bindcar container
2049    #[serde(skip_serializing_if = "Option::is_none")]
2050    pub env_vars: Option<Vec<EnvVar>>,
2051
2052    /// Volumes that can be mounted by the Bindcar container
2053    #[serde(skip_serializing_if = "Option::is_none")]
2054    pub volumes: Option<Vec<Volume>>,
2055
2056    /// Volume mounts for the Bindcar container
2057    #[serde(skip_serializing_if = "Option::is_none")]
2058    pub volume_mounts: Option<Vec<VolumeMount>>,
2059}