1pub mod status_helpers;
12pub mod types;
13
14use status_helpers::update_record_status;
16
17use crate::crd::{
19 AAAARecord, ARecord, CAARecord, CNAMERecord, DNSZone, MXRecord, NSRecord, SRVRecord, TXTRecord,
20};
21use anyhow::{Context, Result};
22use k8s_openapi::apimachinery::pkg::apis::meta::v1::Time;
23
24use kube::{
25 api::{Patch, PatchParams},
26 client::Client,
27 Api, Resource, ResourceExt,
28};
29use serde_json::json;
30use tracing::{debug, info, warn};
31
32async fn get_zone_from_ref(
50 client: &Client,
51 zone_ref: &crate::crd::ZoneReference,
52) -> Result<DNSZone> {
53 let dns_zones_api: Api<DNSZone> = Api::namespaced(client.clone(), &zone_ref.namespace);
54
55 dns_zones_api.get(&zone_ref.name).await.context(format!(
56 "Failed to get DNSZone {}/{}",
57 zone_ref.namespace, zone_ref.name
58 ))
59}
60
61struct RecordReconciliationContext {
65 zone_ref: crate::crd::ZoneReference,
67 primary_refs: Vec<crate::crd::InstanceReference>,
69 current_hash: String,
71}
72
73#[allow(clippy::too_many_lines)]
99async fn prepare_record_reconciliation<T, S>(
100 client: &Client,
101 record: &T,
102 record_type: &str,
103 spec_hashable: &S,
104 bind9_instances_store: &kube::runtime::reflector::Store<crate::crd::Bind9Instance>,
105) -> Result<Option<RecordReconciliationContext>>
106where
107 T: Resource<DynamicType = (), Scope = k8s_openapi::NamespaceResourceScope>
108 + ResourceExt
109 + Clone
110 + std::fmt::Debug
111 + serde::Serialize
112 + for<'de> serde::Deserialize<'de>,
113 S: serde::Serialize,
114{
115 let namespace = record.namespace().unwrap_or_default();
116 let name = record.name_any();
117
118 let record_json = serde_json::to_value(record)?;
120 let status = record_json.get("status");
121
122 let zone_ref = status
123 .and_then(|s| s.get("zoneRef"))
124 .and_then(|z| serde_json::from_value::<crate::crd::ZoneReference>(z.clone()).ok());
125
126 let observed_generation = status
127 .and_then(|s| s.get("observedGeneration"))
128 .and_then(serde_json::Value::as_i64);
129
130 let current_generation = record.meta().generation;
131
132 let Some(zone_ref) = zone_ref else {
134 if !crate::reconcilers::should_reconcile(current_generation, observed_generation) {
136 debug!("Spec unchanged and no zoneRef, skipping reconciliation");
137 return Ok(None);
138 }
139
140 info!(
141 "{} record {}/{} not selected by any DNSZone (no zoneRef in status)",
142 record_type, namespace, name
143 );
144 update_record_status(
145 client,
146 record,
147 "Ready",
148 "False",
149 "NotSelected",
150 "Record not selected by any DNSZone recordsFrom selector",
151 current_generation,
152 None, None, None, )
156 .await?;
157 return Ok(None);
158 };
159
160 let current_hash = crate::ddns::calculate_record_hash(spec_hashable);
162
163 let dnszone = match get_zone_from_ref(client, &zone_ref).await {
165 Ok(zone) => zone,
166 Err(e) => {
167 warn!(
168 "Failed to get DNSZone {}/{} for {} record {}/{}: {}",
169 zone_ref.namespace, zone_ref.name, record_type, namespace, name, e
170 );
171 update_record_status(
172 client,
173 record,
174 "Ready",
175 "False",
176 "ZoneNotFound",
177 &format!(
178 "Referenced DNSZone {}/{} not found: {e}",
179 zone_ref.namespace, zone_ref.name
180 ),
181 current_generation,
182 None, None, None, )
186 .await?;
187 return Ok(None);
188 }
189 };
190
191 let instance_refs = match crate::reconcilers::dnszone::validation::get_instances_from_zone(
193 &dnszone,
194 bind9_instances_store,
195 ) {
196 Ok(refs) => refs,
197 Err(e) => {
198 warn!(
199 "DNSZone {}/{} has no instances assigned for {} record {}/{}: {}",
200 zone_ref.namespace, zone_ref.name, record_type, namespace, name, e
201 );
202 update_record_status(
203 client,
204 record,
205 "Ready",
206 "False",
207 "ZoneNotConfigured",
208 &format!("DNSZone has no instances: {e}"),
209 current_generation,
210 None, None, None, )
214 .await?;
215 return Ok(None);
216 }
217 };
218
219 let primary_refs = match crate::reconcilers::dnszone::primary::filter_primary_instances(
221 client,
222 &instance_refs,
223 )
224 .await
225 {
226 Ok(refs) => refs,
227 Err(e) => {
228 warn!(
229 "Failed to filter primary instances for {} record {}/{}: {}",
230 record_type, namespace, name, e
231 );
232 update_record_status(
233 client,
234 record,
235 "Ready",
236 "False",
237 "InstanceFilterError",
238 &format!("Failed to filter primary instances: {e}"),
239 current_generation,
240 None, None, None, )
244 .await?;
245 return Ok(None);
246 }
247 };
248
249 if primary_refs.is_empty() {
250 warn!(
251 "DNSZone {}/{} has no primary instances for {} record {}/{}",
252 zone_ref.namespace, zone_ref.name, record_type, namespace, name
253 );
254 update_record_status(
255 client,
256 record,
257 "Ready",
258 "False",
259 "NoPrimaryInstances",
260 "DNSZone has no primary instances configured",
261 current_generation,
262 None, None, None, )
266 .await?;
267 return Ok(None);
268 }
269
270 Ok(Some(RecordReconciliationContext {
271 zone_ref,
272 primary_refs,
273 current_hash,
274 }))
275}
276
277trait RecordOperation: Clone + Send + Sync {
308 fn record_type_name(&self) -> &'static str;
310
311 fn add_to_bind9(
326 &self,
327 zone_manager: &crate::bind9::Bind9Manager,
328 zone_name: &str,
329 record_name: &str,
330 ttl: Option<i32>,
331 server: &str,
332 key_data: &crate::bind9::RndcKeyData,
333 ) -> impl std::future::Future<Output = Result<()>> + Send;
334}
335
336trait ReconcilableRecord:
374 Resource<DynamicType = (), Scope = k8s_openapi::NamespaceResourceScope>
375 + ResourceExt
376 + Clone
377 + std::fmt::Debug
378 + serde::Serialize
379 + for<'de> serde::Deserialize<'de>
380 + Send
381 + Sync
382{
383 type Spec: serde::Serialize + Clone;
385
386 type Operation: RecordOperation;
388
389 fn get_spec(&self) -> &Self::Spec;
391
392 fn record_type_name() -> &'static str;
394
395 fn create_operation(spec: &Self::Spec) -> Self::Operation;
397
398 fn get_record_name(spec: &Self::Spec) -> &str;
400
401 fn get_ttl(spec: &Self::Spec) -> Option<i32>;
403}
404
405async fn add_record_to_instances_generic<R>(
429 client: &Client,
430 stores: &crate::context::Stores,
431 instance_refs: &[crate::crd::InstanceReference],
432 zone_name: &str,
433 record_name: &str,
434 ttl: Option<i32>,
435 record_op: R,
436) -> Result<()>
437where
438 R: RecordOperation,
439{
440 use crate::reconcilers::dnszone::helpers::for_each_instance_endpoint;
441
442 let instance_map: std::collections::HashMap<String, String> = instance_refs
444 .iter()
445 .map(|inst| (inst.name.clone(), inst.namespace.clone()))
446 .collect();
447
448 let (_first, _total) = for_each_instance_endpoint(
449 client,
450 instance_refs,
451 true, "dns-tcp", |pod_endpoint, instance_name, rndc_key| {
454 let zone_name = zone_name.to_string();
455 let record_name = record_name.to_string();
456
457 let instance_namespace = instance_map
459 .get(&instance_name)
460 .expect("Instance should be in map")
461 .clone();
462
463 let zone_manager =
465 stores.create_bind9_manager_for_instance(&instance_name, &instance_namespace);
466
467 let record_op_clone = record_op.clone();
469
470 async move {
471 let key_data = rndc_key.expect("RNDC key should be loaded");
472
473 record_op_clone
474 .add_to_bind9(&zone_manager, &zone_name, &record_name, ttl, &pod_endpoint, &key_data)
475 .await
476 .context(format!(
477 "Failed to add {} record {record_name}.{zone_name} to primary {pod_endpoint} (instance: {instance_name})",
478 record_op_clone.record_type_name()
479 ))?;
480
481 Ok(())
482 }
483 },
484 )
485 .await?;
486
487 Ok(())
488}
489
490#[derive(Clone)]
494struct ARecordOp {
495 ipv4_addresses: Vec<String>,
496}
497
498impl RecordOperation for ARecordOp {
499 fn record_type_name(&self) -> &'static str {
500 "A"
501 }
502
503 async fn add_to_bind9(
504 &self,
505 zone_manager: &crate::bind9::Bind9Manager,
506 zone_name: &str,
507 record_name: &str,
508 ttl: Option<i32>,
509 server: &str,
510 key_data: &crate::bind9::RndcKeyData,
511 ) -> Result<()> {
512 zone_manager
513 .add_a_record(
514 zone_name,
515 record_name,
516 &self.ipv4_addresses,
517 ttl,
518 server,
519 key_data,
520 )
521 .await
522 }
523}
524
525impl ReconcilableRecord for ARecord {
527 type Spec = crate::crd::ARecordSpec;
528 type Operation = ARecordOp;
529
530 fn get_spec(&self) -> &Self::Spec {
531 &self.spec
532 }
533
534 fn record_type_name() -> &'static str {
535 "A"
536 }
537
538 fn create_operation(spec: &Self::Spec) -> Self::Operation {
539 ARecordOp {
540 ipv4_addresses: spec.ipv4_addresses.clone(),
541 }
542 }
543
544 fn get_record_name(spec: &Self::Spec) -> &str {
545 &spec.name
546 }
547
548 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
549 spec.ttl
550 }
551}
552
553#[derive(Clone)]
555struct AAAARecordOp {
556 ipv6_addresses: Vec<String>,
557}
558
559impl RecordOperation for AAAARecordOp {
560 fn record_type_name(&self) -> &'static str {
561 "AAAA"
562 }
563
564 async fn add_to_bind9(
565 &self,
566 zone_manager: &crate::bind9::Bind9Manager,
567 zone_name: &str,
568 record_name: &str,
569 ttl: Option<i32>,
570 server: &str,
571 key_data: &crate::bind9::RndcKeyData,
572 ) -> Result<()> {
573 zone_manager
574 .add_aaaa_record(
575 zone_name,
576 record_name,
577 &self.ipv6_addresses,
578 ttl,
579 server,
580 key_data,
581 )
582 .await
583 }
584}
585
586impl ReconcilableRecord for AAAARecord {
588 type Spec = crate::crd::AAAARecordSpec;
589 type Operation = AAAARecordOp;
590
591 fn get_spec(&self) -> &Self::Spec {
592 &self.spec
593 }
594
595 fn record_type_name() -> &'static str {
596 "AAAA"
597 }
598
599 fn create_operation(spec: &Self::Spec) -> Self::Operation {
600 AAAARecordOp {
601 ipv6_addresses: spec.ipv6_addresses.clone(),
602 }
603 }
604
605 fn get_record_name(spec: &Self::Spec) -> &str {
606 &spec.name
607 }
608
609 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
610 spec.ttl
611 }
612}
613
614#[derive(Clone)]
616struct CNAMERecordOp {
617 target: String,
618}
619
620impl RecordOperation for CNAMERecordOp {
621 fn record_type_name(&self) -> &'static str {
622 "CNAME"
623 }
624
625 async fn add_to_bind9(
626 &self,
627 zone_manager: &crate::bind9::Bind9Manager,
628 zone_name: &str,
629 record_name: &str,
630 ttl: Option<i32>,
631 server: &str,
632 key_data: &crate::bind9::RndcKeyData,
633 ) -> Result<()> {
634 zone_manager
635 .add_cname_record(zone_name, record_name, &self.target, ttl, server, key_data)
636 .await
637 }
638}
639
640impl ReconcilableRecord for CNAMERecord {
642 type Spec = crate::crd::CNAMERecordSpec;
643 type Operation = CNAMERecordOp;
644
645 fn get_spec(&self) -> &Self::Spec {
646 &self.spec
647 }
648
649 fn record_type_name() -> &'static str {
650 "CNAME"
651 }
652
653 fn create_operation(spec: &Self::Spec) -> Self::Operation {
654 CNAMERecordOp {
655 target: spec.target.clone(),
656 }
657 }
658
659 fn get_record_name(spec: &Self::Spec) -> &str {
660 &spec.name
661 }
662
663 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
664 spec.ttl
665 }
666}
667
668#[derive(Clone)]
670struct TXTRecordOp {
671 texts: Vec<String>,
672}
673
674impl RecordOperation for TXTRecordOp {
675 fn record_type_name(&self) -> &'static str {
676 "TXT"
677 }
678
679 async fn add_to_bind9(
680 &self,
681 zone_manager: &crate::bind9::Bind9Manager,
682 zone_name: &str,
683 record_name: &str,
684 ttl: Option<i32>,
685 server: &str,
686 key_data: &crate::bind9::RndcKeyData,
687 ) -> Result<()> {
688 zone_manager
689 .add_txt_record(zone_name, record_name, &self.texts, ttl, server, key_data)
690 .await
691 }
692}
693
694impl ReconcilableRecord for TXTRecord {
696 type Spec = crate::crd::TXTRecordSpec;
697 type Operation = TXTRecordOp;
698
699 fn get_spec(&self) -> &Self::Spec {
700 &self.spec
701 }
702
703 fn record_type_name() -> &'static str {
704 "TXT"
705 }
706
707 fn create_operation(spec: &Self::Spec) -> Self::Operation {
708 TXTRecordOp {
709 texts: spec.text.clone(),
710 }
711 }
712
713 fn get_record_name(spec: &Self::Spec) -> &str {
714 &spec.name
715 }
716
717 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
718 spec.ttl
719 }
720}
721
722#[derive(Clone)]
724struct MXRecordOp {
725 priority: i32,
726 mail_server: String,
727}
728
729impl RecordOperation for MXRecordOp {
730 fn record_type_name(&self) -> &'static str {
731 "MX"
732 }
733
734 async fn add_to_bind9(
735 &self,
736 zone_manager: &crate::bind9::Bind9Manager,
737 zone_name: &str,
738 record_name: &str,
739 ttl: Option<i32>,
740 server: &str,
741 key_data: &crate::bind9::RndcKeyData,
742 ) -> Result<()> {
743 zone_manager
744 .add_mx_record(
745 zone_name,
746 record_name,
747 self.priority,
748 &self.mail_server,
749 ttl,
750 server,
751 key_data,
752 )
753 .await
754 }
755}
756
757impl ReconcilableRecord for MXRecord {
759 type Spec = crate::crd::MXRecordSpec;
760 type Operation = MXRecordOp;
761
762 fn get_spec(&self) -> &Self::Spec {
763 &self.spec
764 }
765
766 fn record_type_name() -> &'static str {
767 "MX"
768 }
769
770 fn create_operation(spec: &Self::Spec) -> Self::Operation {
771 MXRecordOp {
772 priority: spec.priority,
773 mail_server: spec.mail_server.clone(),
774 }
775 }
776
777 fn get_record_name(spec: &Self::Spec) -> &str {
778 &spec.name
779 }
780
781 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
782 spec.ttl
783 }
784}
785
786#[derive(Clone)]
788struct NSRecordOp {
789 nameserver: String,
790}
791
792impl RecordOperation for NSRecordOp {
793 fn record_type_name(&self) -> &'static str {
794 "NS"
795 }
796
797 async fn add_to_bind9(
798 &self,
799 zone_manager: &crate::bind9::Bind9Manager,
800 zone_name: &str,
801 record_name: &str,
802 ttl: Option<i32>,
803 server: &str,
804 key_data: &crate::bind9::RndcKeyData,
805 ) -> Result<()> {
806 zone_manager
807 .add_ns_record(
808 zone_name,
809 record_name,
810 &self.nameserver,
811 ttl,
812 server,
813 key_data,
814 )
815 .await
816 }
817}
818
819impl ReconcilableRecord for NSRecord {
821 type Spec = crate::crd::NSRecordSpec;
822 type Operation = NSRecordOp;
823
824 fn get_spec(&self) -> &Self::Spec {
825 &self.spec
826 }
827
828 fn record_type_name() -> &'static str {
829 "NS"
830 }
831
832 fn create_operation(spec: &Self::Spec) -> Self::Operation {
833 NSRecordOp {
834 nameserver: spec.nameserver.clone(),
835 }
836 }
837
838 fn get_record_name(spec: &Self::Spec) -> &str {
839 &spec.name
840 }
841
842 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
843 spec.ttl
844 }
845}
846
847#[derive(Clone)]
849struct SRVRecordOp {
850 priority: i32,
851 weight: i32,
852 port: i32,
853 target: String,
854}
855
856impl RecordOperation for SRVRecordOp {
857 fn record_type_name(&self) -> &'static str {
858 "SRV"
859 }
860
861 async fn add_to_bind9(
862 &self,
863 zone_manager: &crate::bind9::Bind9Manager,
864 zone_name: &str,
865 record_name: &str,
866 ttl: Option<i32>,
867 server: &str,
868 key_data: &crate::bind9::RndcKeyData,
869 ) -> Result<()> {
870 let srv_data = crate::bind9::SRVRecordData {
871 priority: self.priority,
872 weight: self.weight,
873 port: self.port,
874 target: self.target.clone(),
875 ttl,
876 };
877 zone_manager
878 .add_srv_record(zone_name, record_name, &srv_data, server, key_data)
879 .await
880 }
881}
882
883impl ReconcilableRecord for SRVRecord {
885 type Spec = crate::crd::SRVRecordSpec;
886 type Operation = SRVRecordOp;
887
888 fn get_spec(&self) -> &Self::Spec {
889 &self.spec
890 }
891
892 fn record_type_name() -> &'static str {
893 "SRV"
894 }
895
896 fn create_operation(spec: &Self::Spec) -> Self::Operation {
897 SRVRecordOp {
898 priority: spec.priority,
899 weight: spec.weight,
900 port: spec.port,
901 target: spec.target.clone(),
902 }
903 }
904
905 fn get_record_name(spec: &Self::Spec) -> &str {
906 &spec.name
907 }
908
909 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
910 spec.ttl
911 }
912}
913
914#[derive(Clone)]
916struct CAARecordOp {
917 flags: i32,
918 tag: String,
919 value: String,
920}
921
922impl RecordOperation for CAARecordOp {
923 fn record_type_name(&self) -> &'static str {
924 "CAA"
925 }
926
927 async fn add_to_bind9(
928 &self,
929 zone_manager: &crate::bind9::Bind9Manager,
930 zone_name: &str,
931 record_name: &str,
932 ttl: Option<i32>,
933 server: &str,
934 key_data: &crate::bind9::RndcKeyData,
935 ) -> Result<()> {
936 zone_manager
937 .add_caa_record(
938 zone_name,
939 record_name,
940 self.flags,
941 &self.tag,
942 &self.value,
943 ttl,
944 server,
945 key_data,
946 )
947 .await
948 }
949}
950
951impl ReconcilableRecord for CAARecord {
953 type Spec = crate::crd::CAARecordSpec;
954 type Operation = CAARecordOp;
955
956 fn get_spec(&self) -> &Self::Spec {
957 &self.spec
958 }
959
960 fn record_type_name() -> &'static str {
961 "CAA"
962 }
963
964 fn create_operation(spec: &Self::Spec) -> Self::Operation {
965 CAARecordOp {
966 flags: spec.flags,
967 tag: spec.tag.clone(),
968 value: spec.value.clone(),
969 }
970 }
971
972 fn get_record_name(spec: &Self::Spec) -> &str {
973 &spec.name
974 }
975
976 fn get_ttl(spec: &Self::Spec) -> Option<i32> {
977 spec.ttl
978 }
979}
980
981async fn reconcile_record<T>(ctx: std::sync::Arc<crate::context::Context>, record: T) -> Result<()>
1015where
1016 T: ReconcilableRecord,
1017{
1018 let client = ctx.client.clone();
1019 let bind9_instances_store = &ctx.stores.bind9_instances;
1020 let namespace = record.namespace().unwrap_or_default();
1021 let name = record.name_any();
1022
1023 info!(
1024 "Reconciling {}Record: {}/{}",
1025 T::record_type_name(),
1026 namespace,
1027 name
1028 );
1029
1030 let spec = record.get_spec();
1031 let current_generation = record.meta().generation;
1032
1033 let Some(rec_ctx) = prepare_record_reconciliation(
1035 &client,
1036 &record,
1037 T::record_type_name(),
1038 spec,
1039 bind9_instances_store,
1040 )
1041 .await?
1042 else {
1043 return Ok(()); };
1045
1046 let record_op = T::create_operation(spec);
1048
1049 match add_record_to_instances_generic(
1051 &client,
1052 &ctx.stores,
1053 &rec_ctx.primary_refs,
1054 &rec_ctx.zone_ref.zone_name,
1055 T::get_record_name(spec),
1056 T::get_ttl(spec),
1057 record_op,
1058 )
1059 .await
1060 {
1061 Ok(()) => {
1062 info!(
1063 "Successfully added {} record {}.{} via {} primary instance(s)",
1064 T::record_type_name(),
1065 T::get_record_name(spec),
1066 rec_ctx.zone_ref.zone_name,
1067 rec_ctx.primary_refs.len()
1068 );
1069
1070 update_record_reconciled_timestamp(
1072 &client,
1073 &rec_ctx.zone_ref.namespace,
1074 &rec_ctx.zone_ref.name,
1075 &format!("{}Record", T::record_type_name()),
1076 &name,
1077 &namespace,
1078 )
1079 .await?;
1080
1081 update_record_status(
1083 &client,
1084 &record,
1085 "Ready",
1086 "True",
1087 "ReconcileSucceeded",
1088 &format!(
1089 "{} record added to zone {}",
1090 T::record_type_name(),
1091 rec_ctx.zone_ref.zone_name
1092 ),
1093 current_generation,
1094 Some(rec_ctx.current_hash),
1095 Some(chrono::Utc::now().to_rfc3339()),
1096 None, )
1098 .await?;
1099 }
1100 Err(e) => {
1101 warn!(
1102 "Failed to add {} record {}.{}: {}",
1103 T::record_type_name(),
1104 T::get_record_name(spec),
1105 rec_ctx.zone_ref.zone_name,
1106 e
1107 );
1108 update_record_status(
1109 &client,
1110 &record,
1111 "Ready",
1112 "False",
1113 "ReconcileFailed",
1114 &format!("Failed to add record to zone: {e}"),
1115 current_generation,
1116 None, None, None, )
1120 .await?;
1121 }
1122 }
1123
1124 Ok(())
1125}
1126
1127pub async fn reconcile_a_record(
1137 ctx: std::sync::Arc<crate::context::Context>,
1138 record: ARecord,
1139) -> Result<()> {
1140 reconcile_record(ctx.clone(), record.clone()).await?;
1142
1143 let client = ctx.client.clone();
1145 let namespace = record.namespace().unwrap_or_default();
1146 let name = record.name_any();
1147 let api: Api<ARecord> = Api::namespaced(client.clone(), &namespace);
1148
1149 let addresses = record.spec.ipv4_addresses.join(",");
1151
1152 let status_patch = serde_json::json!({
1154 "status": {
1155 "addresses": addresses
1156 }
1157 });
1158
1159 api.patch_status(&name, &PatchParams::default(), &Patch::Merge(&status_patch))
1160 .await
1161 .context("Failed to update addresses in status")?;
1162
1163 Ok(())
1164}
1165
1166pub async fn reconcile_txt_record(
1176 ctx: std::sync::Arc<crate::context::Context>,
1177 record: TXTRecord,
1178) -> Result<()> {
1179 let client = ctx.client.clone();
1180 let bind9_instances_store = &ctx.stores.bind9_instances;
1181 let namespace = record.namespace().unwrap_or_default();
1182 let name = record.name_any();
1183
1184 info!("Reconciling TXTRecord: {}/{}", namespace, name);
1185
1186 let spec = &record.spec;
1187 let current_generation = record.metadata.generation;
1188
1189 let Some(rec_ctx) =
1191 prepare_record_reconciliation(&client, &record, "TXT", spec, bind9_instances_store).await?
1192 else {
1193 return Ok(()); };
1195
1196 let record_op = TXTRecordOp {
1198 texts: spec.text.clone(),
1199 };
1200 match add_record_to_instances_generic(
1201 &client,
1202 &ctx.stores,
1203 &rec_ctx.primary_refs,
1204 &rec_ctx.zone_ref.zone_name,
1205 &spec.name,
1206 spec.ttl,
1207 record_op,
1208 )
1209 .await
1210 {
1211 Ok(()) => {
1212 info!(
1213 "Successfully added TXT record {}.{} via {} primary instance(s)",
1214 spec.name,
1215 rec_ctx.zone_ref.zone_name,
1216 rec_ctx.primary_refs.len()
1217 );
1218
1219 update_record_reconciled_timestamp(
1221 &client,
1222 &rec_ctx.zone_ref.namespace,
1223 &rec_ctx.zone_ref.name,
1224 "TXTRecord",
1225 &name,
1226 &namespace,
1227 )
1228 .await?;
1229
1230 update_record_status(
1231 &client,
1232 &record,
1233 "Ready",
1234 "True",
1235 "ReconcileSucceeded",
1236 &format!("TXT record added to zone {}", rec_ctx.zone_ref.zone_name),
1237 current_generation,
1238 Some(rec_ctx.current_hash),
1239 Some(chrono::Utc::now().to_rfc3339()),
1240 None, )
1242 .await?;
1243 }
1244 Err(e) => {
1245 warn!(
1246 "Failed to add TXT record {}.{}: {}",
1247 spec.name, rec_ctx.zone_ref.zone_name, e
1248 );
1249 update_record_status(
1250 &client,
1251 &record,
1252 "Ready",
1253 "False",
1254 "ReconcileFailed",
1255 &format!("Failed to add record to zone: {e}"),
1256 current_generation,
1257 None, None, None, )
1261 .await?;
1262 }
1263 }
1264
1265 Ok(())
1266}
1267
1268pub async fn reconcile_aaaa_record(
1277 ctx: std::sync::Arc<crate::context::Context>,
1278 record: AAAARecord,
1279) -> Result<()> {
1280 let client = ctx.client.clone();
1281 let bind9_instances_store = &ctx.stores.bind9_instances;
1282 let namespace = record.namespace().unwrap_or_default();
1283 let name = record.name_any();
1284
1285 info!("Reconciling AAAARecord: {}/{}", namespace, name);
1286
1287 let spec = &record.spec;
1288 let current_generation = record.metadata.generation;
1289
1290 let Some(rec_ctx) =
1292 prepare_record_reconciliation(&client, &record, "AAAA", spec, bind9_instances_store)
1293 .await?
1294 else {
1295 return Ok(()); };
1297
1298 let record_op = AAAARecordOp {
1300 ipv6_addresses: spec.ipv6_addresses.clone(),
1301 };
1302 match add_record_to_instances_generic(
1303 &client,
1304 &ctx.stores,
1305 &rec_ctx.primary_refs,
1306 &rec_ctx.zone_ref.zone_name,
1307 &spec.name,
1308 spec.ttl,
1309 record_op,
1310 )
1311 .await
1312 {
1313 Ok(()) => {
1314 info!(
1315 "Successfully added AAAA record {}.{} via {} primary instance(s)",
1316 spec.name,
1317 rec_ctx.zone_ref.zone_name,
1318 rec_ctx.primary_refs.len()
1319 );
1320
1321 update_record_reconciled_timestamp(
1323 &client,
1324 &rec_ctx.zone_ref.namespace,
1325 &rec_ctx.zone_ref.name,
1326 "AAAARecord",
1327 &name,
1328 &namespace,
1329 )
1330 .await?;
1331
1332 let addresses = record.spec.ipv6_addresses.join(",");
1334
1335 update_record_status(
1336 &client,
1337 &record,
1338 "Ready",
1339 "True",
1340 "ReconcileSucceeded",
1341 &format!("AAAA record added to zone {}", rec_ctx.zone_ref.zone_name),
1342 current_generation,
1343 Some(rec_ctx.current_hash),
1344 Some(chrono::Utc::now().to_rfc3339()),
1345 Some(addresses),
1346 )
1347 .await?;
1348 }
1349 Err(e) => {
1350 warn!(
1351 "Failed to add AAAA record {}.{}: {}",
1352 spec.name, rec_ctx.zone_ref.zone_name, e
1353 );
1354 update_record_status(
1355 &client,
1356 &record,
1357 "Ready",
1358 "False",
1359 "ReconcileFailed",
1360 &format!("Failed to add record to zone: {e}"),
1361 current_generation,
1362 None, None, None, )
1366 .await?;
1367 }
1368 }
1369
1370 Ok(())
1371}
1372
1373#[allow(clippy::too_many_lines)]
1383pub async fn reconcile_cname_record(
1384 ctx: std::sync::Arc<crate::context::Context>,
1385 record: CNAMERecord,
1386) -> Result<()> {
1387 let client = ctx.client.clone();
1388 let bind9_instances_store = &ctx.stores.bind9_instances;
1389 let namespace = record.namespace().unwrap_or_default();
1390 let name = record.name_any();
1391
1392 info!("Reconciling CNAMERecord: {}/{}", namespace, name);
1393
1394 let spec = &record.spec;
1395 let current_generation = record.metadata.generation;
1396
1397 let Some(rec_ctx) =
1399 prepare_record_reconciliation(&client, &record, "CNAME", spec, bind9_instances_store)
1400 .await?
1401 else {
1402 return Ok(()); };
1404
1405 let record_op = CNAMERecordOp {
1407 target: spec.target.clone(),
1408 };
1409 match add_record_to_instances_generic(
1410 &client,
1411 &ctx.stores,
1412 &rec_ctx.primary_refs,
1413 &rec_ctx.zone_ref.zone_name,
1414 &spec.name,
1415 spec.ttl,
1416 record_op,
1417 )
1418 .await
1419 {
1420 Ok(()) => {
1421 info!(
1422 "Successfully added CNAME record {}.{} via {} primary instance(s)",
1423 spec.name,
1424 rec_ctx.zone_ref.zone_name,
1425 rec_ctx.primary_refs.len()
1426 );
1427
1428 update_record_reconciled_timestamp(
1430 &client,
1431 &rec_ctx.zone_ref.namespace,
1432 &rec_ctx.zone_ref.name,
1433 "CNAMERecord",
1434 &name,
1435 &namespace,
1436 )
1437 .await?;
1438
1439 update_record_status(
1440 &client,
1441 &record,
1442 "Ready",
1443 "True",
1444 "ReconcileSucceeded",
1445 &format!("CNAME record added to zone {}", rec_ctx.zone_ref.zone_name),
1446 current_generation,
1447 Some(rec_ctx.current_hash),
1448 Some(chrono::Utc::now().to_rfc3339()),
1449 None, )
1451 .await?;
1452 }
1453 Err(e) => {
1454 warn!(
1455 "Failed to add CNAME record {}.{}: {}",
1456 spec.name, rec_ctx.zone_ref.zone_name, e
1457 );
1458 update_record_status(
1459 &client,
1460 &record,
1461 "Ready",
1462 "False",
1463 "ReconcileFailed",
1464 &format!("Failed to add record to zone: {e}"),
1465 current_generation,
1466 None, None, None, )
1470 .await?;
1471 }
1472 }
1473
1474 Ok(())
1475}
1476
1477#[allow(clippy::too_many_lines)]
1487pub async fn reconcile_mx_record(
1488 ctx: std::sync::Arc<crate::context::Context>,
1489 record: MXRecord,
1490) -> Result<()> {
1491 let client = ctx.client.clone();
1492 let bind9_instances_store = &ctx.stores.bind9_instances;
1493 let namespace = record.namespace().unwrap_or_default();
1494 let name = record.name_any();
1495
1496 info!("Reconciling MXRecord: {}/{}", namespace, name);
1497
1498 let spec = &record.spec;
1499 let current_generation = record.metadata.generation;
1500
1501 let Some(rec_ctx) =
1503 prepare_record_reconciliation(&client, &record, "MX", spec, bind9_instances_store).await?
1504 else {
1505 return Ok(()); };
1507
1508 let record_op = MXRecordOp {
1510 priority: spec.priority,
1511 mail_server: spec.mail_server.clone(),
1512 };
1513 match add_record_to_instances_generic(
1514 &client,
1515 &ctx.stores,
1516 &rec_ctx.primary_refs,
1517 &rec_ctx.zone_ref.zone_name,
1518 &spec.name,
1519 spec.ttl,
1520 record_op,
1521 )
1522 .await
1523 {
1524 Ok(()) => {
1525 info!(
1526 "Successfully added MX record {}.{} via {} primary instance(s)",
1527 spec.name,
1528 rec_ctx.zone_ref.zone_name,
1529 rec_ctx.primary_refs.len()
1530 );
1531
1532 update_record_reconciled_timestamp(
1534 &client,
1535 &rec_ctx.zone_ref.namespace,
1536 &rec_ctx.zone_ref.name,
1537 "MXRecord",
1538 &name,
1539 &namespace,
1540 )
1541 .await?;
1542
1543 update_record_status(
1544 &client,
1545 &record,
1546 "Ready",
1547 "True",
1548 "ReconcileSucceeded",
1549 &format!("MX record added to zone {}", rec_ctx.zone_ref.zone_name),
1550 current_generation,
1551 Some(rec_ctx.current_hash),
1552 Some(chrono::Utc::now().to_rfc3339()),
1553 None, )
1555 .await?;
1556 }
1557 Err(e) => {
1558 warn!(
1559 "Failed to add MX record {}.{}: {}",
1560 spec.name, rec_ctx.zone_ref.zone_name, e
1561 );
1562 update_record_status(
1563 &client,
1564 &record,
1565 "Ready",
1566 "False",
1567 "ReconcileFailed",
1568 &format!("Failed to add record to zone: {e}"),
1569 current_generation,
1570 None, None, None, )
1574 .await?;
1575 }
1576 }
1577
1578 Ok(())
1579}
1580
1581#[allow(clippy::too_many_lines)]
1591pub async fn reconcile_ns_record(
1592 ctx: std::sync::Arc<crate::context::Context>,
1593 record: NSRecord,
1594) -> Result<()> {
1595 let client = ctx.client.clone();
1596 let bind9_instances_store = &ctx.stores.bind9_instances;
1597 let namespace = record.namespace().unwrap_or_default();
1598 let name = record.name_any();
1599
1600 info!("Reconciling NSRecord: {}/{}", namespace, name);
1601
1602 let spec = &record.spec;
1603 let current_generation = record.metadata.generation;
1604
1605 let Some(rec_ctx) =
1607 prepare_record_reconciliation(&client, &record, "NS", spec, bind9_instances_store).await?
1608 else {
1609 return Ok(()); };
1611
1612 let record_op = NSRecordOp {
1614 nameserver: spec.nameserver.clone(),
1615 };
1616 match add_record_to_instances_generic(
1617 &client,
1618 &ctx.stores,
1619 &rec_ctx.primary_refs,
1620 &rec_ctx.zone_ref.zone_name,
1621 &spec.name,
1622 spec.ttl,
1623 record_op,
1624 )
1625 .await
1626 {
1627 Ok(()) => {
1628 info!(
1629 "Successfully added NS record {}.{} via {} primary instance(s)",
1630 spec.name,
1631 rec_ctx.zone_ref.zone_name,
1632 rec_ctx.primary_refs.len()
1633 );
1634
1635 update_record_reconciled_timestamp(
1637 &client,
1638 &rec_ctx.zone_ref.namespace,
1639 &rec_ctx.zone_ref.name,
1640 "NSRecord",
1641 &name,
1642 &namespace,
1643 )
1644 .await?;
1645
1646 update_record_status(
1647 &client,
1648 &record,
1649 "Ready",
1650 "True",
1651 "ReconcileSucceeded",
1652 &format!("NS record added to zone {}", rec_ctx.zone_ref.zone_name),
1653 current_generation,
1654 Some(rec_ctx.current_hash),
1655 Some(chrono::Utc::now().to_rfc3339()),
1656 None, )
1658 .await?;
1659 }
1660 Err(e) => {
1661 warn!(
1662 "Failed to add NS record {}.{}: {}",
1663 spec.name, rec_ctx.zone_ref.zone_name, e
1664 );
1665 update_record_status(
1666 &client,
1667 &record,
1668 "Ready",
1669 "False",
1670 "ReconcileFailed",
1671 &format!("Failed to add record to zone: {e}"),
1672 current_generation,
1673 None, None, None, )
1677 .await?;
1678 }
1679 }
1680
1681 Ok(())
1682}
1683
1684#[allow(clippy::too_many_lines)]
1694pub async fn reconcile_srv_record(
1695 ctx: std::sync::Arc<crate::context::Context>,
1696 record: SRVRecord,
1697) -> Result<()> {
1698 let client = ctx.client.clone();
1699 let bind9_instances_store = &ctx.stores.bind9_instances;
1700 let namespace = record.namespace().unwrap_or_default();
1701 let name = record.name_any();
1702
1703 info!("Reconciling SRVRecord: {}/{}", namespace, name);
1704
1705 let spec = &record.spec;
1706 let current_generation = record.metadata.generation;
1707
1708 let Some(rec_ctx) =
1710 prepare_record_reconciliation(&client, &record, "SRV", spec, bind9_instances_store).await?
1711 else {
1712 return Ok(()); };
1714
1715 let record_op = SRVRecordOp {
1717 priority: spec.priority,
1718 weight: spec.weight,
1719 port: spec.port,
1720 target: spec.target.clone(),
1721 };
1722 match add_record_to_instances_generic(
1723 &client,
1724 &ctx.stores,
1725 &rec_ctx.primary_refs,
1726 &rec_ctx.zone_ref.zone_name,
1727 &spec.name,
1728 spec.ttl,
1729 record_op,
1730 )
1731 .await
1732 {
1733 Ok(()) => {
1734 info!(
1735 "Successfully added SRV record {}.{} via {} primary instance(s)",
1736 spec.name,
1737 rec_ctx.zone_ref.zone_name,
1738 rec_ctx.primary_refs.len()
1739 );
1740
1741 update_record_reconciled_timestamp(
1743 &client,
1744 &rec_ctx.zone_ref.namespace,
1745 &rec_ctx.zone_ref.name,
1746 "SRVRecord",
1747 &name,
1748 &namespace,
1749 )
1750 .await?;
1751
1752 update_record_status(
1753 &client,
1754 &record,
1755 "Ready",
1756 "True",
1757 "ReconcileSucceeded",
1758 &format!("SRV record added to zone {}", rec_ctx.zone_ref.zone_name),
1759 current_generation,
1760 Some(rec_ctx.current_hash),
1761 Some(chrono::Utc::now().to_rfc3339()),
1762 None, )
1764 .await?;
1765 }
1766 Err(e) => {
1767 warn!(
1768 "Failed to add SRV record {}.{}: {}",
1769 spec.name, rec_ctx.zone_ref.zone_name, e
1770 );
1771 update_record_status(
1772 &client,
1773 &record,
1774 "Ready",
1775 "False",
1776 "ReconcileFailed",
1777 &format!("Failed to add record to zone: {e}"),
1778 current_generation,
1779 None, None, None, )
1783 .await?;
1784 }
1785 }
1786
1787 Ok(())
1788}
1789
1790#[allow(clippy::too_many_lines)]
1801pub async fn reconcile_caa_record(
1802 ctx: std::sync::Arc<crate::context::Context>,
1803 record: CAARecord,
1804) -> Result<()> {
1805 let client = ctx.client.clone();
1806 let bind9_instances_store = &ctx.stores.bind9_instances;
1807 let namespace = record.namespace().unwrap_or_default();
1808 let name = record.name_any();
1809
1810 info!("Reconciling CAARecord: {}/{}", namespace, name);
1811
1812 let spec = &record.spec;
1813 let current_generation = record.metadata.generation;
1814
1815 let Some(rec_ctx) =
1817 prepare_record_reconciliation(&client, &record, "CAA", spec, bind9_instances_store).await?
1818 else {
1819 return Ok(()); };
1821
1822 let record_op = CAARecordOp {
1824 flags: spec.flags,
1825 tag: spec.tag.clone(),
1826 value: spec.value.clone(),
1827 };
1828 match add_record_to_instances_generic(
1829 &client,
1830 &ctx.stores,
1831 &rec_ctx.primary_refs,
1832 &rec_ctx.zone_ref.zone_name,
1833 &spec.name,
1834 spec.ttl,
1835 record_op,
1836 )
1837 .await
1838 {
1839 Ok(()) => {
1840 info!(
1841 "Successfully added CAA record {}.{} via {} primary instance(s)",
1842 spec.name,
1843 rec_ctx.zone_ref.zone_name,
1844 rec_ctx.primary_refs.len()
1845 );
1846
1847 update_record_reconciled_timestamp(
1849 &client,
1850 &rec_ctx.zone_ref.namespace,
1851 &rec_ctx.zone_ref.name,
1852 "CAARecord",
1853 &name,
1854 &namespace,
1855 )
1856 .await?;
1857
1858 update_record_status(
1859 &client,
1860 &record,
1861 "Ready",
1862 "True",
1863 "ReconcileSucceeded",
1864 &format!("CAA record added to zone {}", rec_ctx.zone_ref.zone_name),
1865 current_generation,
1866 Some(rec_ctx.current_hash),
1867 Some(chrono::Utc::now().to_rfc3339()),
1868 None, )
1870 .await?;
1871 }
1872 Err(e) => {
1873 warn!(
1874 "Failed to add CAA record {}.{}: {}",
1875 spec.name, rec_ctx.zone_ref.zone_name, e
1876 );
1877 update_record_status(
1878 &client,
1879 &record,
1880 "Ready",
1881 "False",
1882 "ReconcileFailed",
1883 &format!("Failed to add record to zone: {e}"),
1884 current_generation,
1885 None, None, None, )
1889 .await?;
1890 }
1891 }
1892
1893 Ok(())
1894}
1895
1896#[allow(clippy::too_many_lines)]
1924pub async fn delete_record<T>(
1925 client: &Client,
1926 record: &T,
1927 record_type: &str,
1928 record_type_hickory: hickory_client::rr::RecordType,
1929 stores: &crate::context::Stores,
1930) -> Result<()>
1931where
1932 T: Resource<DynamicType = (), Scope = k8s_openapi::NamespaceResourceScope>
1933 + ResourceExt
1934 + Clone
1935 + std::fmt::Debug
1936 + serde::Serialize
1937 + for<'de> serde::Deserialize<'de>,
1938{
1939 let namespace = record.namespace().unwrap_or_default();
1940 let name = record.name_any();
1941
1942 info!("Deleting {} record: {}/{}", record_type, namespace, name);
1943
1944 let status = serde_json::to_value(record)
1946 .ok()
1947 .and_then(|v| v.get("status").cloned());
1948
1949 let zone_ref = status
1950 .as_ref()
1951 .and_then(|s| s.get("zoneRef"))
1952 .cloned()
1953 .and_then(|z| serde_json::from_value::<crate::crd::ZoneReference>(z).ok());
1954
1955 let Some(zone_ref) = zone_ref else {
1957 info!(
1958 "{} record {}/{} has no zoneRef - was never added to DNS or already cleaned up",
1959 record_type, namespace, name
1960 );
1961 return Ok(());
1962 };
1963
1964 let dnszone = match get_zone_from_ref(client, &zone_ref).await {
1966 Ok(zone) => zone,
1967 Err(e) => {
1968 warn!(
1969 "DNSZone {}/{} not found for {} record {}/{}: {}. Allowing deletion anyway.",
1970 zone_ref.namespace, zone_ref.name, record_type, namespace, name, e
1971 );
1972 return Ok(());
1973 }
1974 };
1975
1976 let instance_refs = match crate::reconcilers::dnszone::validation::get_instances_from_zone(
1978 &dnszone,
1979 &stores.bind9_instances,
1980 ) {
1981 Ok(refs) => refs,
1982 Err(e) => {
1983 warn!(
1984 "DNSZone {}/{} has no instances for {} record {}/{}: {}. Allowing deletion anyway.",
1985 zone_ref.namespace, zone_ref.name, record_type, namespace, name, e
1986 );
1987 return Ok(());
1988 }
1989 };
1990
1991 let primary_refs = match crate::reconcilers::dnszone::primary::filter_primary_instances(
1993 client,
1994 &instance_refs,
1995 )
1996 .await
1997 {
1998 Ok(refs) => refs,
1999 Err(e) => {
2000 warn!(
2001 "Failed to filter primary instances for {} record {}/{}: {}. Allowing deletion anyway.",
2002 record_type, namespace, name, e
2003 );
2004 return Ok(());
2005 }
2006 };
2007
2008 if primary_refs.is_empty() {
2009 warn!(
2010 "No primary instances found for {} record {}/{}. Allowing deletion anyway.",
2011 record_type, namespace, name
2012 );
2013 return Ok(());
2014 }
2015
2016 let instance_map: std::collections::HashMap<String, String> = primary_refs
2019 .iter()
2020 .map(|inst| (inst.name.clone(), inst.namespace.clone()))
2021 .collect();
2022
2023 let (_first_endpoint, total_endpoints) =
2024 crate::reconcilers::dnszone::helpers::for_each_instance_endpoint(
2025 client,
2026 &primary_refs,
2027 true, "dns-tcp", |pod_endpoint, instance_name, rndc_key| {
2030 let zone_name = zone_ref.zone_name.clone();
2031 let record_name_str = if let Some(record_spec) = serde_json::to_value(record)
2032 .ok()
2033 .and_then(|v| v.get("spec").cloned())
2034 {
2035 record_spec
2036 .get("name")
2037 .and_then(|n| n.as_str())
2038 .unwrap_or(&name)
2039 .to_string()
2040 } else {
2041 name.clone()
2042 };
2043 let instance_namespace = instance_map
2044 .get(&instance_name)
2045 .expect("Instance should be in map")
2046 .clone();
2047
2048 let zone_manager = stores.create_bind9_manager_for_instance(&instance_name, &instance_namespace);
2050
2051 async move {
2052 let key_data = rndc_key.expect("RNDC key should be loaded");
2053
2054 if let Err(e) = zone_manager
2056 .delete_record(
2057 &zone_name,
2058 &record_name_str,
2059 record_type_hickory,
2060 &pod_endpoint,
2061 &key_data,
2062 )
2063 .await
2064 {
2065 warn!(
2066 "Failed to delete {} record {}.{} from endpoint {} (instance: {}): {}. Continuing with deletion anyway.",
2067 record_type, record_name_str, zone_name, pod_endpoint, instance_name, e
2068 );
2069 } else {
2070 info!(
2071 "Successfully deleted {} record {}.{} from endpoint {} (instance: {})",
2072 record_type, record_name_str, zone_name, pod_endpoint, instance_name
2073 );
2074 }
2075
2076 Ok(())
2077 }
2078 },
2079 )
2080 .await?;
2081
2082 info!(
2083 "Successfully deleted {} record {}/{} from {} primary endpoint(s)",
2084 record_type, namespace, name, total_endpoints
2085 );
2086
2087 Ok(())
2088}
2089
2090pub async fn update_record_reconciled_timestamp(
2111 client: &Client,
2112 zone_namespace: &str,
2113 zone_name: &str,
2114 record_kind: &str,
2115 record_name: &str,
2116 record_namespace: &str,
2117) -> Result<()> {
2118 let api: Api<DNSZone> = Api::namespaced(client.clone(), zone_namespace);
2119
2120 let mut zone = api.get(zone_name).await?;
2122
2123 let mut found = false;
2125 if let Some(status) = &mut zone.status {
2126 for record_ref in &mut status.records {
2127 if record_ref.kind == record_kind
2128 && record_ref.name == record_name
2129 && record_ref.namespace == record_namespace
2130 {
2131 record_ref.last_reconciled_at = Some(Time(k8s_openapi::jiff::Timestamp::now()));
2132 found = true;
2133 break;
2134 }
2135 }
2136 }
2137
2138 if !found {
2139 warn!(
2140 "Record {} {}/{} not found in DNSZone {}/{} selectedRecords[] - cannot update timestamp",
2141 record_kind, record_namespace, record_name, zone_namespace, zone_name
2142 );
2143 return Ok(());
2144 }
2145
2146 let status_patch = json!({
2148 "status": {
2149 "selectedRecords": zone.status.as_ref().map(|s| &s.records)
2150 }
2151 });
2152
2153 api.patch_status(
2154 zone_name,
2155 &PatchParams::default(),
2156 &Patch::Merge(status_patch),
2157 )
2158 .await?;
2159
2160 info!(
2161 "Updated lastReconciledAt for {} record {}/{} in zone {}/{}",
2162 record_kind, record_namespace, record_name, zone_namespace, zone_name
2163 );
2164
2165 Ok(())
2166}