1#[allow(clippy::wildcard_imports)]
10use super::types::*;
11
12use crate::bind9::Bind9Manager;
13use crate::bind9_resources::{
14 build_configmap, build_deployment, build_service, build_service_account,
15};
16use crate::constants::{API_GROUP_VERSION, KIND_BIND9_INSTANCE};
17use crate::reconcilers::resources::create_or_apply;
18use anyhow::Context as _;
19
20pub(super) fn resolve_full_rndc_config(
34 instance: &Bind9Instance,
35 cluster: Option<&Bind9Cluster>,
36 _cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
37) -> crate::crd::RndcKeyConfig {
38 use super::config::{resolve_rndc_config, resolve_rndc_config_from_deprecated};
39
40 let instance_config = instance.spec.rndc_key.as_ref();
42
43 let role_config = cluster.and_then(|c| match instance.spec.role {
46 crate::crd::ServerRole::Primary => c
47 .spec
48 .common
49 .primary
50 .as_ref()
51 .and_then(|p| p.rndc_key.as_ref()),
52 crate::crd::ServerRole::Secondary => c
53 .spec
54 .common
55 .secondary
56 .as_ref()
57 .and_then(|s| s.rndc_key.as_ref()),
58 });
59
60 #[allow(deprecated)]
65 let deprecated_instance_ref = instance.spec.rndc_secret_ref.as_ref();
66
67 let resolved = resolve_rndc_config(instance_config, role_config, None);
69
70 if instance_config.is_none() && role_config.is_none() {
72 if deprecated_instance_ref.is_some() {
74 return resolve_rndc_config_from_deprecated(
75 None,
76 deprecated_instance_ref,
77 instance.spec.role.clone(),
78 );
79 }
80 }
81
82 resolved
83}
84
85#[allow(clippy::too_many_lines)] pub(super) async fn create_or_update_resources(
87 client: &Client,
88 namespace: &str,
89 name: &str,
90 instance: &Bind9Instance,
91) -> Result<(
92 Option<Bind9Cluster>,
93 Option<crate::crd::ClusterBind9Provider>,
94 Option<Secret>, )> {
96 debug!(
97 namespace = %namespace,
98 name = %name,
99 "Creating or updating Kubernetes resources"
100 );
101
102 let cluster = if instance.spec.cluster_ref.is_empty() {
104 debug!("No cluster reference, proceeding with standalone instance");
105 None
106 } else {
107 debug!(cluster_ref = %instance.spec.cluster_ref, "Fetching Bind9Cluster");
108 let cluster_api: Api<Bind9Cluster> = Api::namespaced(client.clone(), namespace);
109 match cluster_api.get(&instance.spec.cluster_ref).await {
110 Ok(cluster) => {
111 debug!(
112 cluster_name = %instance.spec.cluster_ref,
113 "Successfully fetched Bind9Cluster"
114 );
115 info!(
116 "Found Bind9Cluster: {}/{}",
117 namespace, instance.spec.cluster_ref
118 );
119 Some(cluster)
120 }
121 Err(e) => {
122 warn!(
123 "Failed to fetch Bind9Cluster {}/{}: {}. Proceeding with instance-only config.",
124 namespace, instance.spec.cluster_ref, e
125 );
126 None
127 }
128 }
129 };
130
131 let cluster_provider = if cluster.is_none() && !instance.spec.cluster_ref.is_empty() {
133 debug!(cluster_ref = %instance.spec.cluster_ref, "Fetching ClusterBind9Provider");
134 let cluster_provider_api: Api<crate::crd::ClusterBind9Provider> = Api::all(client.clone());
135 match cluster_provider_api.get(&instance.spec.cluster_ref).await {
136 Ok(gc) => {
137 debug!(
138 cluster_name = %instance.spec.cluster_ref,
139 "Successfully fetched ClusterBind9Provider"
140 );
141 info!("Found ClusterBind9Provider: {}", instance.spec.cluster_ref);
142 Some(gc)
143 }
144 Err(e) => {
145 warn!(
146 "Failed to fetch ClusterBind9Provider {}: {}. Proceeding with instance-only config.",
147 instance.spec.cluster_ref, e
148 );
149 None
150 }
151 }
152 } else {
153 None
154 };
155
156 validate_user_pod_shape(instance, cluster.as_ref(), cluster_provider.as_ref())
161 .context("user-supplied Pod shape rejected")?;
162
163 let rndc_config =
165 resolve_full_rndc_config(instance, cluster.as_ref(), cluster_provider.as_ref());
166 debug!(
167 "Resolved RNDC config: auto_rotate={}, rotate_after={}",
168 rndc_config.auto_rotate, rndc_config.rotate_after
169 );
170
171 debug!("Step 1: Creating/updating ServiceAccount");
173 create_or_update_service_account(client, namespace, instance).await?;
174
175 debug!("Step 2: Creating/updating RNDC Secret with rotation support");
177 let secret_name =
178 create_or_update_rndc_secret_with_config(client, namespace, name, instance, &rndc_config)
179 .await?;
180
181 let secret = if rndc_config.auto_rotate {
183 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
184 secret_api.get(&secret_name).await.ok()
185 } else {
186 None
187 };
188
189 debug!("Step 3: Creating/updating ConfigMap");
191 create_or_update_configmap(
192 client,
193 namespace,
194 name,
195 instance,
196 cluster.as_ref(),
197 cluster_provider.as_ref(),
198 )
199 .await?;
200
201 debug!("Step 4: Creating/updating Deployment");
203 create_or_update_deployment(
204 client,
205 namespace,
206 name,
207 instance,
208 cluster.as_ref(),
209 cluster_provider.as_ref(),
210 )
211 .await?;
212
213 debug!("Step 5: Creating/updating Service");
215 create_or_update_service(
216 client,
217 namespace,
218 name,
219 instance,
220 cluster.as_ref(),
221 cluster_provider.as_ref(),
222 )
223 .await?;
224
225 debug!("Successfully created/updated all resources");
226 Ok((cluster, cluster_provider, secret))
227}
228
229async fn create_or_update_service_account(
231 client: &Client,
232 namespace: &str,
233 instance: &Bind9Instance,
234) -> Result<()> {
235 let service_account = build_service_account(namespace, instance);
236 create_or_apply(client, namespace, &service_account, "bindy-controller").await
237}
238
239#[allow(dead_code)] #[allow(clippy::too_many_lines)] async fn create_or_update_rndc_secret_with_config(
265 client: &Client,
266 namespace: &str,
267 name: &str,
268 instance: &Bind9Instance,
269 config: &crate::crd::RndcKeyConfig,
270) -> Result<String> {
271 use chrono::Utc;
272
273 if let Some(ref secret_ref) = config.secret_ref {
275 info!(
276 "Using existing Secret reference: {}/{}",
277 namespace, secret_ref.name
278 );
279 return Ok(secret_ref.name.clone());
280 }
281
282 let secret_name = if let Some(ref secret_spec) = config.secret {
284 secret_spec.metadata.name.clone()
286 } else {
287 format!("{name}-rndc-key")
289 };
290
291 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
292
293 match secret_api.get(&secret_name).await {
295 Ok(existing_secret) => {
296 if let Some(ref data) = existing_secret.data {
298 if !data.contains_key("key-name")
299 || !data.contains_key("algorithm")
300 || !data.contains_key("secret")
301 {
302 warn!(
303 "RNDC Secret {}/{} is missing required keys, will recreate",
304 namespace, secret_name
305 );
306 secret_api
307 .delete(&secret_name, &kube::api::DeleteParams::default())
308 .await?;
309 }
311 } else {
312 warn!(
313 "RNDC Secret {}/{} has no data, will recreate",
314 namespace, secret_name
315 );
316 secret_api
317 .delete(&secret_name, &kube::api::DeleteParams::default())
318 .await?;
319 }
321
322 let has_annotations = existing_secret
324 .metadata
325 .annotations
326 .as_ref()
327 .and_then(|a| a.get(crate::constants::ANNOTATION_RNDC_CREATED_AT))
328 .is_some();
329
330 if config.auto_rotate && !has_annotations {
331 info!(
332 "RNDC Secret {}/{} missing rotation annotations, adding them",
333 namespace, secret_name
334 );
335 add_rotation_annotations_to_secret(&secret_api, &secret_name, config).await?;
336 return Ok(secret_name);
337 }
338
339 if config.auto_rotate && should_rotate_secret(&existing_secret, config)? {
341 info!(
342 "RNDC Secret {}/{} rotation is due, rotating",
343 namespace, secret_name
344 );
345 rotate_rndc_secret(
346 client,
347 namespace,
348 &secret_name,
349 config,
350 instance,
351 &existing_secret,
352 )
353 .await?;
354 return Ok(secret_name);
355 }
356
357 if let Some(ref data) = existing_secret.data {
359 if data.contains_key("algorithm") {
360 let current_algorithm =
361 std::str::from_utf8(&data.get("algorithm").unwrap().0).unwrap_or("unknown");
362 let desired_algorithm = config.algorithm.as_str();
363
364 if current_algorithm == desired_algorithm {
365 info!(
366 "RNDC Secret {}/{} exists and is valid, skipping creation",
367 namespace, secret_name
368 );
369 return Ok(secret_name);
370 }
371 warn!(
372 "RNDC Secret {}/{} algorithm mismatch (current: {}, desired: {}), will recreate",
373 namespace, secret_name, current_algorithm, desired_algorithm
374 );
375 secret_api
376 .delete(&secret_name, &kube::api::DeleteParams::default())
377 .await?;
378 }
380 }
381 }
382 Err(_) => {
383 info!(
384 "RNDC Secret {}/{} does not exist, creating",
385 namespace, secret_name
386 );
387 }
388 }
389
390 if let Some(_secret_spec) = &config.secret {
392 info!("Creating RNDC Secret from inline spec with rotation enabled");
395 }
396
397 let mut key_data = Bind9Manager::generate_rndc_key();
399 key_data.name = "bindy-operator".to_string();
400 key_data.algorithm = config.algorithm.clone();
401
402 let created_at = Utc::now();
404 let rotate_after = if config.auto_rotate {
405 crate::bind9::duration::parse_duration(&config.rotate_after).ok()
406 } else {
407 None
408 };
409
410 let secret = crate::bind9::rndc::create_rndc_secret_with_annotations(
412 namespace,
413 &secret_name,
414 &key_data,
415 created_at,
416 rotate_after,
417 0, );
419
420 let owner_ref = OwnerReference {
422 api_version: API_GROUP_VERSION.to_string(),
423 kind: KIND_BIND9_INSTANCE.to_string(),
424 name: name.to_string(),
425 uid: instance.metadata.uid.clone().unwrap_or_default(),
426 controller: Some(true),
427 block_owner_deletion: Some(true),
428 };
429
430 let mut secret_with_owner = secret;
431 secret_with_owner
432 .metadata
433 .owner_references
434 .get_or_insert_with(Vec::new)
435 .push(owner_ref);
436
437 if config.auto_rotate {
439 info!(
440 "Creating RNDC Secret {}/{} with rotation enabled (rotate after: {})",
441 namespace, secret_name, config.rotate_after
442 );
443 } else {
444 info!(
445 "Creating RNDC Secret {}/{} without rotation",
446 namespace, secret_name
447 );
448 }
449
450 secret_api
451 .create(&PostParams::default(), &secret_with_owner)
452 .await?;
453
454 Ok(secret_name)
455}
456
457#[allow(dead_code)] async fn create_or_update_rndc_secret(
463 client: &Client,
464 namespace: &str,
465 name: &str,
466 instance: &Bind9Instance,
467) -> Result<()> {
468 let secret_name = format!("{name}-rndc-key");
469 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
470
471 match secret_api.get(&secret_name).await {
473 Ok(existing_secret) => {
474 info!(
476 "RNDC Secret {}/{} already exists, skipping",
477 namespace, secret_name
478 );
479 if let Some(ref data) = existing_secret.data {
481 if !data.contains_key("key-name")
482 || !data.contains_key("algorithm")
483 || !data.contains_key("secret")
484 {
485 warn!(
486 "RNDC Secret {}/{} is missing required keys, will recreate",
487 namespace, secret_name
488 );
489 secret_api
491 .delete(&secret_name, &kube::api::DeleteParams::default())
492 .await?;
493 } else {
494 return Ok(());
495 }
496 } else {
497 warn!(
498 "RNDC Secret {}/{} has no data, will recreate",
499 namespace, secret_name
500 );
501 secret_api
502 .delete(&secret_name, &kube::api::DeleteParams::default())
503 .await?;
504 }
505 }
506 Err(_) => {
507 info!(
508 "RNDC Secret {}/{} does not exist, creating",
509 namespace, secret_name
510 );
511 }
512 }
513
514 let mut key_data = Bind9Manager::generate_rndc_key();
516 key_data.name = "bindy-operator".to_string();
517
518 let secret_data = Bind9Manager::create_rndc_secret_data(&key_data);
520
521 let owner_ref = OwnerReference {
523 api_version: API_GROUP_VERSION.to_string(),
524 kind: KIND_BIND9_INSTANCE.to_string(),
525 name: name.to_string(),
526 uid: instance.metadata.uid.clone().unwrap_or_default(),
527 controller: Some(true),
528 block_owner_deletion: Some(true),
529 };
530
531 let secret = Secret {
533 metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
534 name: Some(secret_name.clone()),
535 namespace: Some(namespace.to_string()),
536 owner_references: Some(vec![owner_ref]),
537 ..Default::default()
538 },
539 string_data: Some(secret_data),
540 ..Default::default()
541 };
542
543 info!("Creating RNDC Secret {}/{}", namespace, secret_name);
545 secret_api.create(&PostParams::default(), &secret).await?;
546
547 Ok(())
548}
549
550async fn add_rotation_annotations_to_secret(
564 secret_api: &Api<Secret>,
565 secret_name: &str,
566 config: &crate::crd::RndcKeyConfig,
567) -> Result<()> {
568 use chrono::Utc;
569 use kube::api::{Patch, PatchParams};
570 use std::collections::BTreeMap;
571
572 let created_at = Utc::now();
573 let rotate_after = crate::bind9::duration::parse_duration(&config.rotate_after)?;
574 let rotate_at = created_at + chrono::Duration::from_std(rotate_after)?;
575
576 let mut annotations = BTreeMap::new();
577 annotations.insert(
578 crate::constants::ANNOTATION_RNDC_CREATED_AT.to_string(),
579 created_at.to_rfc3339(),
580 );
581 annotations.insert(
582 crate::constants::ANNOTATION_RNDC_ROTATE_AT.to_string(),
583 rotate_at.to_rfc3339(),
584 );
585 annotations.insert(
586 crate::constants::ANNOTATION_RNDC_ROTATION_COUNT.to_string(),
587 "0".to_string(),
588 );
589
590 let patch = serde_json::json!({
591 "metadata": {
592 "annotations": annotations
593 }
594 });
595
596 info!(
597 "Adding rotation annotations to existing Secret {} (rotate at: {})",
598 secret_name,
599 rotate_at.to_rfc3339()
600 );
601
602 secret_api
603 .patch(
604 secret_name,
605 &PatchParams::apply("bindy-operator"),
606 &Patch::Merge(&patch),
607 )
608 .await?;
609
610 Ok(())
611}
612
613pub(super) fn should_rotate_secret(
634 secret: &Secret,
635 config: &crate::crd::RndcKeyConfig,
636) -> Result<bool> {
637 use chrono::Utc;
638
639 if !config.auto_rotate {
641 return Ok(false);
642 }
643
644 let Some(annotations) = &secret.metadata.annotations else {
646 debug!("Secret has no annotations, rotation not due");
647 return Ok(false);
648 };
649
650 let (created_at, rotate_at, _rotation_count) =
651 crate::bind9::rndc::parse_rotation_annotations(annotations)?;
652
653 let now = Utc::now();
654
655 let time_since_creation = now.signed_duration_since(created_at);
657 if time_since_creation.num_hours() < crate::constants::MIN_TIME_BETWEEN_ROTATIONS_HOURS {
658 debug!(
659 "Skipping rotation - Secret was created/rotated {} minutes ago (min 1 hour required)",
660 time_since_creation.num_minutes()
661 );
662 return Ok(false);
663 }
664
665 Ok(crate::bind9::rndc::is_rotation_due(rotate_at, now))
667}
668
669#[allow(dead_code)] async fn rotate_rndc_secret(
693 client: &Client,
694 namespace: &str,
695 secret_name: &str,
696 config: &crate::crd::RndcKeyConfig,
697 instance: &Bind9Instance,
698 existing_secret: &Secret,
699) -> Result<()> {
700 use chrono::Utc;
701
702 let annotations = existing_secret
704 .metadata
705 .annotations
706 .as_ref()
707 .context("Secret missing annotations")?;
708
709 let (_created_at, _rotate_at, rotation_count) =
710 crate::bind9::rndc::parse_rotation_annotations(annotations)?;
711
712 let new_rotation_count = rotation_count + 1;
714
715 info!(
716 "Rotating RNDC Secret {}/{} (rotation #{})",
717 namespace, secret_name, new_rotation_count
718 );
719
720 let mut key_data = Bind9Manager::generate_rndc_key();
722 key_data.name = "bindy-operator".to_string();
723 key_data.algorithm = config.algorithm.clone();
724
725 let created_at = Utc::now();
727 let rotate_after = crate::bind9::duration::parse_duration(&config.rotate_after)?;
728
729 let new_secret = crate::bind9::rndc::create_rndc_secret_with_annotations(
731 namespace,
732 secret_name,
733 &key_data,
734 created_at,
735 Some(rotate_after),
736 new_rotation_count,
737 );
738
739 let mut updated_secret = new_secret;
741 updated_secret
742 .metadata
743 .owner_references
744 .clone_from(&existing_secret.metadata.owner_references);
745
746 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
748 secret_api
749 .replace(secret_name, &PostParams::default(), &updated_secret)
750 .await?;
751
752 info!(
753 "Successfully rotated RNDC Secret {}/{} (rotation #{})",
754 namespace, secret_name, new_rotation_count
755 );
756
757 trigger_deployment_rollout(client, namespace, &instance.name_any()).await?;
759
760 Ok(())
761}
762
763async fn trigger_deployment_rollout(
775 client: &Client,
776 namespace: &str,
777 instance_name: &str,
778) -> Result<()> {
779 use chrono::Utc;
780 use serde_json::json;
781
782 let deployment_api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
783
784 let patch = json!({
786 "spec": {
787 "template": {
788 "metadata": {
789 "annotations": {
790 crate::constants::ANNOTATION_RNDC_ROTATED_AT: Utc::now().to_rfc3339()
791 }
792 }
793 }
794 }
795 });
796
797 deployment_api
798 .patch(
799 instance_name,
800 &PatchParams::default(),
801 &kube::api::Patch::Merge(&patch),
802 )
803 .await?;
804
805 info!(
806 "Triggered Deployment {}/{} rollout after RNDC rotation",
807 namespace, instance_name
808 );
809
810 Ok(())
811}
812
813async fn create_or_update_configmap(
819 client: &Client,
820 namespace: &str,
821 name: &str,
822 instance: &Bind9Instance,
823 cluster: Option<&Bind9Cluster>,
824 _cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
825) -> Result<()> {
826 if !instance.spec.cluster_ref.is_empty() {
829 debug!(
830 "Instance {}/{} belongs to cluster '{}', using cluster ConfigMap",
831 namespace, name, instance.spec.cluster_ref
832 );
833 return Ok(());
834 }
835
836 info!(
838 "Instance {}/{} is standalone, creating instance-specific ConfigMap",
839 namespace, name
840 );
841
842 let role_allow_transfer = cluster.and_then(|c| match instance.spec.role {
846 crate::crd::ServerRole::Primary => c
847 .spec
848 .common
849 .primary
850 .as_ref()
851 .and_then(|p| p.allow_transfer.as_ref()),
852 crate::crd::ServerRole::Secondary => c
853 .spec
854 .common
855 .secondary
856 .as_ref()
857 .and_then(|s| s.allow_transfer.as_ref()),
858 });
859
860 if let Some(configmap) =
863 build_configmap(name, namespace, instance, cluster, role_allow_transfer)?
864 {
865 let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
866 let cm_name = format!("{name}-config");
867
868 if (cm_api.get(&cm_name).await).is_ok() {
869 info!("Updating ConfigMap {}/{}", namespace, cm_name);
871 cm_api
872 .replace(&cm_name, &PostParams::default(), &configmap)
873 .await?;
874 } else {
875 info!("Creating ConfigMap {}/{}", namespace, cm_name);
877 cm_api.create(&PostParams::default(), &configmap).await?;
878 }
879 } else {
880 info!(
881 "Using custom ConfigMaps for {}/{}, skipping ConfigMap creation",
882 namespace, name
883 );
884 }
885
886 Ok(())
887}
888
889fn deployment_needs_update(current: &Deployment, desired: &Deployment) -> bool {
898 let desired_replicas = desired.spec.as_ref().and_then(|s| s.replicas);
900 let current_replicas = current.spec.as_ref().and_then(|s| s.replicas);
901
902 if desired_replicas != current_replicas {
903 debug!(
904 "Replicas changed: current={:?}, desired={:?}",
905 current_replicas, desired_replicas
906 );
907 return true;
908 }
909
910 let current_api_container = current
912 .spec
913 .as_ref()
914 .and_then(|s| s.template.spec.as_ref())
915 .and_then(|pod_spec| {
916 pod_spec
917 .containers
918 .iter()
919 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
920 });
921
922 let desired_api_container = desired
924 .spec
925 .as_ref()
926 .and_then(|s| s.template.spec.as_ref())
927 .and_then(|pod_spec| {
928 pod_spec
929 .containers
930 .iter()
931 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
932 });
933
934 if let (Some(current_api), Some(desired_api)) = (current_api_container, desired_api_container) {
936 if current_api.image != desired_api.image {
938 debug!(
939 "API container image changed: current={:?}, desired={:?}",
940 current_api.image, desired_api.image
941 );
942 return true;
943 }
944
945 if current_api.env != desired_api.env {
947 debug!("API container env changed");
948 return true;
949 }
950
951 if current_api.image_pull_policy != desired_api.image_pull_policy {
953 debug!(
954 "API container imagePullPolicy changed: current={:?}, desired={:?}",
955 current_api.image_pull_policy, desired_api.image_pull_policy
956 );
957 return true;
958 }
959
960 if current_api.resources != desired_api.resources {
962 debug!("API container resources changed");
963 return true;
964 }
965 } else if current_api_container.is_some() != desired_api_container.is_some() {
966 debug!("API container existence changed");
968 return true;
969 }
970
971 false
972}
973
974async fn create_or_update_deployment(
976 client: &Client,
977 namespace: &str,
978 name: &str,
979 instance: &Bind9Instance,
980 cluster: Option<&Bind9Cluster>,
981 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
982) -> Result<()> {
983 let deployment = build_deployment(name, namespace, instance, cluster, cluster_provider);
984 let api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
985
986 if api.get(name).await.is_err() {
988 info!("Creating Deployment {}/{}", namespace, name);
989 api.create(&PostParams::default(), &deployment).await?;
990 return Ok(());
991 }
992
993 debug!(
995 "Checking if Deployment {}/{} needs updating",
996 namespace, name
997 );
998
999 let current_deployment = api.get(name).await?;
1001
1002 if !deployment_needs_update(¤t_deployment, &deployment) {
1004 debug!(
1005 "Deployment {}/{} is up to date, skipping patch",
1006 namespace, name
1007 );
1008 return Ok(());
1009 }
1010
1011 info!("Patching Deployment {}/{}", namespace, name);
1013
1014 let api_container = deployment
1015 .spec
1016 .as_ref()
1017 .and_then(|s| s.template.spec.as_ref())
1018 .and_then(|pod_spec| {
1019 pod_spec
1020 .containers
1021 .iter()
1022 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
1023 });
1024
1025 let mut patch_containers = vec![];
1026
1027 patch_containers.push(json!({
1029 "name": crate::constants::CONTAINER_NAME_BIND9
1030 }));
1031
1032 if let Some(api) = api_container {
1034 let mut api_patch = json!({
1035 "name": crate::constants::CONTAINER_NAME_BINDCAR
1036 });
1037
1038 if let Some(ref image) = api.image {
1040 api_patch["image"] = json!(image);
1041 }
1042
1043 if let Some(ref env) = api.env {
1045 api_patch["env"] = json!(env);
1046 }
1047
1048 if let Some(ref pull_policy) = api.image_pull_policy {
1050 api_patch["imagePullPolicy"] = json!(pull_policy);
1051 }
1052
1053 if let Some(ref resources) = api.resources {
1055 api_patch["resources"] = json!(resources);
1056 }
1057
1058 patch_containers.push(api_patch);
1059 }
1060
1061 let labels = deployment.metadata.labels.as_ref();
1063 let pod_labels = deployment
1064 .spec
1065 .as_ref()
1066 .and_then(|s| s.template.metadata.as_ref())
1067 .and_then(|m| m.labels.as_ref());
1068
1069 let mut patch = json!({
1073 "spec": {
1074 "replicas": deployment.spec.as_ref().and_then(|s| s.replicas),
1075 "template": {
1076 "spec": {
1077 "containers": patch_containers,
1078 "$setElementOrder/containers": [
1079 {"name": crate::constants::CONTAINER_NAME_BIND9},
1080 {"name": crate::constants::CONTAINER_NAME_BINDCAR}
1081 ]
1082 }
1083 }
1084 }
1085 });
1086
1087 if let Some(labels) = labels {
1091 patch["metadata"] = json!({"labels": labels});
1092 }
1093
1094 if let Some(pod_labels) = pod_labels {
1097 patch["spec"]["template"]["metadata"] = json!({"labels": pod_labels});
1098 }
1099
1100 api.patch(name, &PatchParams::default(), &Patch::Strategic(&patch))
1101 .await?;
1102
1103 Ok(())
1104}
1105
1106async fn create_or_update_service(
1108 client: &Client,
1109 namespace: &str,
1110 name: &str,
1111 instance: &Bind9Instance,
1112 cluster: Option<&Bind9Cluster>,
1113 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1114) -> Result<()> {
1115 let custom_spec = cluster
1117 .and_then(|c| match instance.spec.role {
1118 crate::crd::ServerRole::Primary => c
1119 .spec
1120 .common
1121 .primary
1122 .as_ref()
1123 .and_then(|p| p.service.as_ref()),
1124 crate::crd::ServerRole::Secondary => c
1125 .spec
1126 .common
1127 .secondary
1128 .as_ref()
1129 .and_then(|s| s.service.as_ref()),
1130 })
1131 .or_else(|| {
1132 cluster_provider.and_then(|gc| match instance.spec.role {
1134 crate::crd::ServerRole::Primary => gc
1135 .spec
1136 .common
1137 .primary
1138 .as_ref()
1139 .and_then(|p| p.service.as_ref()),
1140 crate::crd::ServerRole::Secondary => gc
1141 .spec
1142 .common
1143 .secondary
1144 .as_ref()
1145 .and_then(|s| s.service.as_ref()),
1146 })
1147 });
1148
1149 let service = build_service(name, namespace, instance, custom_spec);
1150 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1151
1152 if let Ok(existing) = svc_api.get(name).await {
1153 info!("Updating Service {}/{}", namespace, name);
1155 let mut updated_service = service;
1156 if let Some(ref mut spec) = updated_service.spec {
1157 if let Some(ref existing_spec) = existing.spec {
1158 spec.cluster_ip.clone_from(&existing_spec.cluster_ip);
1159 spec.cluster_ips.clone_from(&existing_spec.cluster_ips);
1160 }
1161 }
1162 svc_api
1163 .replace(name, &PostParams::default(), &updated_service)
1164 .await?;
1165 } else {
1166 info!("Creating Service {}/{}", namespace, name);
1168 svc_api.create(&PostParams::default(), &service).await?;
1169 }
1170
1171 Ok(())
1172}
1173
1174pub async fn delete_bind9instance(ctx: Arc<Context>, instance: Bind9Instance) -> Result<()> {
1195 let namespace = instance.namespace().unwrap_or_default();
1196 let name = instance.name_any();
1197
1198 info!("Deleting Bind9Instance: {}/{}", namespace, name);
1199
1200 delete_resources(&ctx.client, &namespace, &name).await?;
1202
1203 info!("Successfully deleted resources for {}/{}", namespace, name);
1204
1205 Ok(())
1206}
1207
1208pub(super) async fn delete_resources(client: &Client, namespace: &str, name: &str) -> Result<()> {
1210 let delete_params = kube::api::DeleteParams::default();
1211
1212 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1214 match svc_api.delete(name, &delete_params).await {
1215 Ok(_) => info!("Deleted Service {}/{}", namespace, name),
1216 Err(e) => warn!("Failed to delete Service {}/{}: {}", namespace, name, e),
1217 }
1218
1219 let deploy_api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
1221 match deploy_api.delete(name, &delete_params).await {
1222 Ok(_) => info!("Deleted Deployment {}/{}", namespace, name),
1223 Err(e) => warn!("Failed to delete Deployment {}/{}: {}", namespace, name, e),
1224 }
1225
1226 let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
1228 let cm_name = format!("{name}-config");
1229 match cm_api.delete(&cm_name, &delete_params).await {
1230 Ok(_) => info!("Deleted ConfigMap {}/{}", namespace, cm_name),
1231 Err(e) => warn!(
1232 "Failed to delete ConfigMap {}/{}: {}",
1233 namespace, cm_name, e
1234 ),
1235 }
1236
1237 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
1239 let secret_name = format!("{name}-rndc-key");
1240 match secret_api.delete(&secret_name, &delete_params).await {
1241 Ok(_) => info!("Deleted Secret {}/{}", namespace, secret_name),
1242 Err(e) => warn!(
1243 "Failed to delete Secret {}/{}: {}",
1244 namespace, secret_name, e
1245 ),
1246 }
1247
1248 let sa_api: Api<ServiceAccount> = Api::namespaced(client.clone(), namespace);
1250 let sa_name = crate::constants::BIND9_SERVICE_ACCOUNT;
1251 match sa_api.get(sa_name).await {
1252 Ok(sa) => {
1253 let is_owner = sa
1255 .metadata
1256 .owner_references
1257 .as_ref()
1258 .is_some_and(|owners| owners.iter().any(|owner| owner.name == name));
1259
1260 if is_owner {
1261 match sa_api.delete(sa_name, &delete_params).await {
1262 Ok(_) => info!("Deleted ServiceAccount {}/{}", namespace, sa_name),
1263 Err(e) => warn!(
1264 "Failed to delete ServiceAccount {}/{}: {}",
1265 namespace, sa_name, e
1266 ),
1267 }
1268 } else {
1269 debug!(
1270 "ServiceAccount {}/{} is not owned by this instance, skipping deletion",
1271 namespace, sa_name
1272 );
1273 }
1274 }
1275 Err(e) => {
1276 debug!(
1277 "ServiceAccount {}/{} does not exist or cannot be retrieved: {}",
1278 namespace, sa_name, e
1279 );
1280 }
1281 }
1282
1283 Ok(())
1284}
1285
1286#[cfg(test)]
1292pub(super) fn validate_user_pod_shape_for_test(
1293 instance: &Bind9Instance,
1294 cluster: Option<&Bind9Cluster>,
1295 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1296) -> anyhow::Result<()> {
1297 validate_user_pod_shape(instance, cluster, cluster_provider)
1298}
1299
1300fn validate_user_pod_shape(
1314 instance: &Bind9Instance,
1315 cluster: Option<&Bind9Cluster>,
1316 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1317) -> anyhow::Result<()> {
1318 use crate::safe_volume::{
1319 validate_optional_user_volume_mounts, validate_optional_user_volumes,
1320 };
1321
1322 validate_optional_user_volumes(instance.spec.volumes.as_ref())
1324 .with_context(|| format!("Bind9Instance {} spec.volumes", instance.name_any()))?;
1325 validate_optional_user_volume_mounts(instance.spec.volume_mounts.as_ref())
1326 .with_context(|| format!("Bind9Instance {} spec.volumeMounts", instance.name_any()))?;
1327
1328 if let Some(c) = cluster {
1330 validate_optional_user_volumes(c.spec.common.volumes.as_ref()).with_context(|| {
1331 format!(
1332 "Bind9Cluster {}/{} spec.volumes",
1333 c.namespace().unwrap_or_default(),
1334 c.name_any(),
1335 )
1336 })?;
1337 validate_optional_user_volume_mounts(c.spec.common.volume_mounts.as_ref()).with_context(
1338 || {
1339 format!(
1340 "Bind9Cluster {}/{} spec.volumeMounts",
1341 c.namespace().unwrap_or_default(),
1342 c.name_any(),
1343 )
1344 },
1345 )?;
1346 }
1347 if let Some(p) = cluster_provider {
1348 validate_optional_user_volumes(p.spec.common.volumes.as_ref())
1349 .with_context(|| format!("ClusterBind9Provider {} spec.volumes", p.name_any()))?;
1350 validate_optional_user_volume_mounts(p.spec.common.volume_mounts.as_ref())
1351 .with_context(|| format!("ClusterBind9Provider {} spec.volumeMounts", p.name_any()))?;
1352 }
1353 Ok(())
1354}
1355
1356#[cfg(test)]
1357#[path = "resources_tests.rs"]
1358mod resources_tests;