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}