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 &secret_name,
211 )
212 .await?;
213
214 debug!("Step 5: Creating/updating Service");
216 create_or_update_service(
217 client,
218 namespace,
219 name,
220 instance,
221 cluster.as_ref(),
222 cluster_provider.as_ref(),
223 )
224 .await?;
225
226 debug!("Successfully created/updated all resources");
227 Ok((cluster, cluster_provider, secret))
228}
229
230async fn create_or_update_service_account(
232 client: &Client,
233 namespace: &str,
234 instance: &Bind9Instance,
235) -> Result<()> {
236 let service_account = build_service_account(namespace, instance);
237 create_or_apply(client, namespace, &service_account, "bindy-controller").await
238}
239
240const RNDC_SECRET_REQUIRED_KEYS: [&str; 3] = ["key-name", "algorithm", "secret"];
242
243#[derive(Debug, PartialEq, Eq)]
246pub(super) enum RndcSecretAction {
247 Keep,
249 AddRotationAnnotations,
252 Rotate,
254 Recreate(String),
257}
258
259pub(super) fn evaluate_existing_rndc_secret(
281 secret: &Secret,
282 config: &crate::crd::RndcKeyConfig,
283) -> Result<RndcSecretAction> {
284 let Some(data) = secret.data.as_ref() else {
286 return Ok(RndcSecretAction::Recreate("Secret has no data".to_string()));
287 };
288 if RNDC_SECRET_REQUIRED_KEYS
289 .iter()
290 .any(|key| !data.contains_key(*key))
291 {
292 return Ok(RndcSecretAction::Recreate(
293 "Secret is missing required keys".to_string(),
294 ));
295 }
296
297 let has_annotations = secret
299 .metadata
300 .annotations
301 .as_ref()
302 .and_then(|a| a.get(crate::constants::ANNOTATION_RNDC_CREATED_AT))
303 .is_some();
304 if config.auto_rotate && !has_annotations {
305 return Ok(RndcSecretAction::AddRotationAnnotations);
306 }
307
308 if config.auto_rotate && should_rotate_secret(secret, config)? {
309 return Ok(RndcSecretAction::Rotate);
310 }
311
312 let current_algorithm = data.get("algorithm").map_or_else(
314 || "unknown".to_string(),
315 |v| String::from_utf8_lossy(&v.0).into_owned(),
316 );
317 let desired_algorithm = config.algorithm.as_str();
318 if current_algorithm != desired_algorithm {
319 return Ok(RndcSecretAction::Recreate(format!(
320 "algorithm mismatch (current: {current_algorithm}, desired: {desired_algorithm})"
321 )));
322 }
323
324 Ok(RndcSecretAction::Keep)
325}
326
327#[allow(dead_code)] #[allow(clippy::too_many_lines)] async fn create_or_update_rndc_secret_with_config(
353 client: &Client,
354 namespace: &str,
355 name: &str,
356 instance: &Bind9Instance,
357 config: &crate::crd::RndcKeyConfig,
358) -> Result<String> {
359 use chrono::Utc;
360
361 if let Some(ref secret_ref) = config.secret_ref {
363 info!(
364 "Using existing Secret reference: {}/{}",
365 namespace, secret_ref.name
366 );
367 return Ok(secret_ref.name.clone());
368 }
369
370 let secret_name = if let Some(ref secret_spec) = config.secret {
372 secret_spec.metadata.name.clone()
374 } else {
375 format!("{name}-rndc-key")
377 };
378
379 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
380
381 match secret_api.get(&secret_name).await {
386 Ok(existing_secret) => match evaluate_existing_rndc_secret(&existing_secret, config)? {
387 RndcSecretAction::Keep => {
388 info!(
389 "RNDC Secret {}/{} exists and is valid, skipping creation",
390 namespace, secret_name
391 );
392 return Ok(secret_name);
393 }
394 RndcSecretAction::AddRotationAnnotations => {
395 info!(
396 "RNDC Secret {}/{} missing rotation annotations, adding them",
397 namespace, secret_name
398 );
399 add_rotation_annotations_to_secret(&secret_api, &secret_name, config).await?;
400 return Ok(secret_name);
401 }
402 RndcSecretAction::Rotate => {
403 info!(
404 "RNDC Secret {}/{} rotation is due, rotating",
405 namespace, secret_name
406 );
407 rotate_rndc_secret(
408 client,
409 namespace,
410 &secret_name,
411 config,
412 instance,
413 &existing_secret,
414 )
415 .await?;
416 return Ok(secret_name);
417 }
418 RndcSecretAction::Recreate(reason) => {
419 warn!(
420 "RNDC Secret {}/{} will be recreated: {}",
421 namespace, secret_name, reason
422 );
423 secret_api
424 .delete(&secret_name, &kube::api::DeleteParams::default())
425 .await?;
426 }
428 },
429 Err(_) => {
430 info!(
431 "RNDC Secret {}/{} does not exist, creating",
432 namespace, secret_name
433 );
434 }
435 }
436
437 if let Some(_secret_spec) = &config.secret {
439 info!("Creating RNDC Secret from inline spec with rotation enabled");
442 }
443
444 let mut key_data = Bind9Manager::generate_rndc_key();
446 key_data.name = "bindy-operator".to_string();
447 key_data.algorithm = config.algorithm.clone();
448
449 let created_at = Utc::now();
451 let rotate_after = if config.auto_rotate {
452 crate::bind9::duration::parse_duration(&config.rotate_after).ok()
453 } else {
454 None
455 };
456
457 let secret = crate::bind9::rndc::create_rndc_secret_with_annotations(
459 namespace,
460 &secret_name,
461 &key_data,
462 created_at,
463 rotate_after,
464 0, );
466
467 let owner_ref = OwnerReference {
469 api_version: API_GROUP_VERSION.to_string(),
470 kind: KIND_BIND9_INSTANCE.to_string(),
471 name: name.to_string(),
472 uid: instance.metadata.uid.clone().unwrap_or_default(),
473 controller: Some(true),
474 block_owner_deletion: Some(true),
475 };
476
477 let mut secret_with_owner = secret;
478 secret_with_owner
479 .metadata
480 .owner_references
481 .get_or_insert_with(Vec::new)
482 .push(owner_ref);
483
484 if config.auto_rotate {
486 info!(
487 "Creating RNDC Secret {}/{} with rotation enabled (rotate after: {})",
488 namespace, secret_name, config.rotate_after
489 );
490 } else {
491 info!(
492 "Creating RNDC Secret {}/{} without rotation",
493 namespace, secret_name
494 );
495 }
496
497 secret_api
498 .create(&PostParams::default(), &secret_with_owner)
499 .await?;
500
501 Ok(secret_name)
502}
503
504#[allow(dead_code)] async fn create_or_update_rndc_secret(
510 client: &Client,
511 namespace: &str,
512 name: &str,
513 instance: &Bind9Instance,
514) -> Result<()> {
515 let secret_name = format!("{name}-rndc-key");
516 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
517
518 match secret_api.get(&secret_name).await {
520 Ok(existing_secret) => {
521 info!(
523 "RNDC Secret {}/{} already exists, skipping",
524 namespace, secret_name
525 );
526 if let Some(ref data) = existing_secret.data {
528 if !data.contains_key("key-name")
529 || !data.contains_key("algorithm")
530 || !data.contains_key("secret")
531 {
532 warn!(
533 "RNDC Secret {}/{} is missing required keys, will recreate",
534 namespace, secret_name
535 );
536 secret_api
538 .delete(&secret_name, &kube::api::DeleteParams::default())
539 .await?;
540 } else {
541 return Ok(());
542 }
543 } else {
544 warn!(
545 "RNDC Secret {}/{} has no data, will recreate",
546 namespace, secret_name
547 );
548 secret_api
549 .delete(&secret_name, &kube::api::DeleteParams::default())
550 .await?;
551 }
552 }
553 Err(_) => {
554 info!(
555 "RNDC Secret {}/{} does not exist, creating",
556 namespace, secret_name
557 );
558 }
559 }
560
561 let mut key_data = Bind9Manager::generate_rndc_key();
563 key_data.name = "bindy-operator".to_string();
564
565 let secret_data = Bind9Manager::create_rndc_secret_data(&key_data);
567
568 let owner_ref = OwnerReference {
570 api_version: API_GROUP_VERSION.to_string(),
571 kind: KIND_BIND9_INSTANCE.to_string(),
572 name: name.to_string(),
573 uid: instance.metadata.uid.clone().unwrap_or_default(),
574 controller: Some(true),
575 block_owner_deletion: Some(true),
576 };
577
578 let secret = Secret {
580 metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
581 name: Some(secret_name.clone()),
582 namespace: Some(namespace.to_string()),
583 owner_references: Some(vec![owner_ref]),
584 ..Default::default()
585 },
586 string_data: Some(secret_data),
587 ..Default::default()
588 };
589
590 info!("Creating RNDC Secret {}/{}", namespace, secret_name);
592 secret_api.create(&PostParams::default(), &secret).await?;
593
594 Ok(())
595}
596
597async fn add_rotation_annotations_to_secret(
611 secret_api: &Api<Secret>,
612 secret_name: &str,
613 config: &crate::crd::RndcKeyConfig,
614) -> Result<()> {
615 use chrono::Utc;
616 use kube::api::{Patch, PatchParams};
617 use std::collections::BTreeMap;
618
619 let created_at = Utc::now();
620 let rotate_after = crate::bind9::duration::parse_duration(&config.rotate_after)?;
621 let rotate_at = created_at + chrono::Duration::from_std(rotate_after)?;
622
623 let mut annotations = BTreeMap::new();
624 annotations.insert(
625 crate::constants::ANNOTATION_RNDC_CREATED_AT.to_string(),
626 created_at.to_rfc3339(),
627 );
628 annotations.insert(
629 crate::constants::ANNOTATION_RNDC_ROTATE_AT.to_string(),
630 rotate_at.to_rfc3339(),
631 );
632 annotations.insert(
633 crate::constants::ANNOTATION_RNDC_ROTATION_COUNT.to_string(),
634 "0".to_string(),
635 );
636
637 let patch = serde_json::json!({
638 "metadata": {
639 "annotations": annotations
640 }
641 });
642
643 info!(
644 "Adding rotation annotations to existing Secret {} (rotate at: {})",
645 secret_name,
646 rotate_at.to_rfc3339()
647 );
648
649 secret_api
650 .patch(
651 secret_name,
652 &PatchParams::apply("bindy-operator"),
653 &Patch::Merge(&patch),
654 )
655 .await?;
656
657 Ok(())
658}
659
660pub(super) fn should_rotate_secret(
681 secret: &Secret,
682 config: &crate::crd::RndcKeyConfig,
683) -> Result<bool> {
684 use chrono::Utc;
685
686 if !config.auto_rotate {
688 return Ok(false);
689 }
690
691 let Some(annotations) = &secret.metadata.annotations else {
693 debug!("Secret has no annotations, rotation not due");
694 return Ok(false);
695 };
696
697 let (created_at, rotate_at, _rotation_count) =
698 crate::bind9::rndc::parse_rotation_annotations(annotations)?;
699
700 let now = Utc::now();
701
702 let time_since_creation = now.signed_duration_since(created_at);
704 if time_since_creation.num_hours() < crate::constants::MIN_TIME_BETWEEN_ROTATIONS_HOURS {
705 debug!(
706 "Skipping rotation - Secret was created/rotated {} minutes ago (min 1 hour required)",
707 time_since_creation.num_minutes()
708 );
709 return Ok(false);
710 }
711
712 Ok(crate::bind9::rndc::is_rotation_due(rotate_at, now))
714}
715
716#[allow(dead_code)] async fn rotate_rndc_secret(
740 client: &Client,
741 namespace: &str,
742 secret_name: &str,
743 config: &crate::crd::RndcKeyConfig,
744 instance: &Bind9Instance,
745 existing_secret: &Secret,
746) -> Result<()> {
747 use chrono::Utc;
748
749 let annotations = existing_secret
751 .metadata
752 .annotations
753 .as_ref()
754 .context("Secret missing annotations")?;
755
756 let (_created_at, _rotate_at, rotation_count) =
757 crate::bind9::rndc::parse_rotation_annotations(annotations)?;
758
759 let new_rotation_count = rotation_count + 1;
761
762 info!(
763 "Rotating RNDC Secret {}/{} (rotation #{})",
764 namespace, secret_name, new_rotation_count
765 );
766
767 let mut key_data = Bind9Manager::generate_rndc_key();
769 key_data.name = "bindy-operator".to_string();
770 key_data.algorithm = config.algorithm.clone();
771
772 let created_at = Utc::now();
774 let rotate_after = crate::bind9::duration::parse_duration(&config.rotate_after)?;
775
776 let new_secret = crate::bind9::rndc::create_rndc_secret_with_annotations(
778 namespace,
779 secret_name,
780 &key_data,
781 created_at,
782 Some(rotate_after),
783 new_rotation_count,
784 );
785
786 let mut updated_secret = new_secret;
788 updated_secret
789 .metadata
790 .owner_references
791 .clone_from(&existing_secret.metadata.owner_references);
792
793 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
795 secret_api
796 .replace(secret_name, &PostParams::default(), &updated_secret)
797 .await?;
798
799 info!(
800 "Successfully rotated RNDC Secret {}/{} (rotation #{})",
801 namespace, secret_name, new_rotation_count
802 );
803
804 trigger_deployment_rollout(client, namespace, &instance.name_any()).await?;
806
807 Ok(())
808}
809
810async fn trigger_deployment_rollout(
822 client: &Client,
823 namespace: &str,
824 instance_name: &str,
825) -> Result<()> {
826 use chrono::Utc;
827 use serde_json::json;
828
829 let deployment_api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
830
831 let patch = json!({
833 "spec": {
834 "template": {
835 "metadata": {
836 "annotations": {
837 crate::constants::ANNOTATION_RNDC_ROTATED_AT: Utc::now().to_rfc3339()
838 }
839 }
840 }
841 }
842 });
843
844 deployment_api
845 .patch(
846 instance_name,
847 &PatchParams::default(),
848 &kube::api::Patch::Merge(&patch),
849 )
850 .await?;
851
852 info!(
853 "Triggered Deployment {}/{} rollout after RNDC rotation",
854 namespace, instance_name
855 );
856
857 Ok(())
858}
859
860async fn create_or_update_configmap(
866 client: &Client,
867 namespace: &str,
868 name: &str,
869 instance: &Bind9Instance,
870 cluster: Option<&Bind9Cluster>,
871 _cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
872) -> Result<()> {
873 if !instance.spec.cluster_ref.is_empty() {
876 debug!(
877 "Instance {}/{} belongs to cluster '{}', using cluster ConfigMap",
878 namespace, name, instance.spec.cluster_ref
879 );
880 return Ok(());
881 }
882
883 info!(
885 "Instance {}/{} is standalone, creating instance-specific ConfigMap",
886 namespace, name
887 );
888
889 let role_allow_transfer = cluster.and_then(|c| match instance.spec.role {
893 crate::crd::ServerRole::Primary => c
894 .spec
895 .common
896 .primary
897 .as_ref()
898 .and_then(|p| p.allow_transfer.as_ref()),
899 crate::crd::ServerRole::Secondary => c
900 .spec
901 .common
902 .secondary
903 .as_ref()
904 .and_then(|s| s.allow_transfer.as_ref()),
905 });
906
907 let configmap = build_configmap(name, namespace, instance, cluster, role_allow_transfer)?;
913 let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
914 let cm_name = format!("{name}-config");
915
916 if (cm_api.get(&cm_name).await).is_ok() {
917 info!("Updating ConfigMap {}/{}", namespace, cm_name);
919 cm_api
920 .replace(&cm_name, &PostParams::default(), &configmap)
921 .await?;
922 return Ok(());
923 }
924
925 info!("Creating ConfigMap {}/{}", namespace, cm_name);
927 cm_api.create(&PostParams::default(), &configmap).await?;
928
929 Ok(())
930}
931
932fn deployment_needs_update(current: &Deployment, desired: &Deployment) -> bool {
941 let desired_replicas = desired.spec.as_ref().and_then(|s| s.replicas);
943 let current_replicas = current.spec.as_ref().and_then(|s| s.replicas);
944
945 if desired_replicas != current_replicas {
946 debug!(
947 "Replicas changed: current={:?}, desired={:?}",
948 current_replicas, desired_replicas
949 );
950 return true;
951 }
952
953 let current_api_container = current
955 .spec
956 .as_ref()
957 .and_then(|s| s.template.spec.as_ref())
958 .and_then(|pod_spec| {
959 pod_spec
960 .containers
961 .iter()
962 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
963 });
964
965 let desired_api_container = desired
967 .spec
968 .as_ref()
969 .and_then(|s| s.template.spec.as_ref())
970 .and_then(|pod_spec| {
971 pod_spec
972 .containers
973 .iter()
974 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
975 });
976
977 if let (Some(current_api), Some(desired_api)) = (current_api_container, desired_api_container) {
979 if current_api.image != desired_api.image {
981 debug!(
982 "API container image changed: current={:?}, desired={:?}",
983 current_api.image, desired_api.image
984 );
985 return true;
986 }
987
988 if current_api.env != desired_api.env {
990 debug!("API container env changed");
991 return true;
992 }
993
994 if current_api.image_pull_policy != desired_api.image_pull_policy {
996 debug!(
997 "API container imagePullPolicy changed: current={:?}, desired={:?}",
998 current_api.image_pull_policy, desired_api.image_pull_policy
999 );
1000 return true;
1001 }
1002
1003 if current_api.resources != desired_api.resources {
1005 debug!("API container resources changed");
1006 return true;
1007 }
1008 } else if current_api_container.is_some() != desired_api_container.is_some() {
1009 debug!("API container existence changed");
1011 return true;
1012 }
1013
1014 false
1015}
1016
1017#[allow(clippy::too_many_arguments)]
1024async fn create_or_update_deployment(
1025 client: &Client,
1026 namespace: &str,
1027 name: &str,
1028 instance: &Bind9Instance,
1029 cluster: Option<&Bind9Cluster>,
1030 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1031 rndc_secret_name: &str,
1032) -> Result<()> {
1033 let deployment = build_deployment(
1034 name,
1035 namespace,
1036 instance,
1037 cluster,
1038 cluster_provider,
1039 rndc_secret_name,
1040 );
1041 let api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
1042
1043 if api.get(name).await.is_err() {
1045 info!("Creating Deployment {}/{}", namespace, name);
1046 api.create(&PostParams::default(), &deployment).await?;
1047 return Ok(());
1048 }
1049
1050 debug!(
1052 "Checking if Deployment {}/{} needs updating",
1053 namespace, name
1054 );
1055
1056 let current_deployment = api.get(name).await?;
1058
1059 if !deployment_needs_update(¤t_deployment, &deployment) {
1061 debug!(
1062 "Deployment {}/{} is up to date, skipping patch",
1063 namespace, name
1064 );
1065 return Ok(());
1066 }
1067
1068 info!("Patching Deployment {}/{}", namespace, name);
1070
1071 let api_container = deployment
1072 .spec
1073 .as_ref()
1074 .and_then(|s| s.template.spec.as_ref())
1075 .and_then(|pod_spec| {
1076 pod_spec
1077 .containers
1078 .iter()
1079 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
1080 });
1081
1082 let mut patch_containers = vec![];
1083
1084 patch_containers.push(json!({
1086 "name": crate::constants::CONTAINER_NAME_BIND9
1087 }));
1088
1089 if let Some(api) = api_container {
1091 let mut api_patch = json!({
1092 "name": crate::constants::CONTAINER_NAME_BINDCAR
1093 });
1094
1095 if let Some(ref image) = api.image {
1097 api_patch["image"] = json!(image);
1098 }
1099
1100 if let Some(ref env) = api.env {
1102 api_patch["env"] = json!(env);
1103 }
1104
1105 if let Some(ref pull_policy) = api.image_pull_policy {
1107 api_patch["imagePullPolicy"] = json!(pull_policy);
1108 }
1109
1110 if let Some(ref resources) = api.resources {
1112 api_patch["resources"] = json!(resources);
1113 }
1114
1115 patch_containers.push(api_patch);
1116 }
1117
1118 let labels = deployment.metadata.labels.as_ref();
1120 let pod_labels = deployment
1121 .spec
1122 .as_ref()
1123 .and_then(|s| s.template.metadata.as_ref())
1124 .and_then(|m| m.labels.as_ref());
1125
1126 let mut patch = json!({
1130 "spec": {
1131 "replicas": deployment.spec.as_ref().and_then(|s| s.replicas),
1132 "template": {
1133 "spec": {
1134 "containers": patch_containers,
1135 "$setElementOrder/containers": [
1136 {"name": crate::constants::CONTAINER_NAME_BIND9},
1137 {"name": crate::constants::CONTAINER_NAME_BINDCAR}
1138 ]
1139 }
1140 }
1141 }
1142 });
1143
1144 if let Some(labels) = labels {
1148 patch["metadata"] = json!({"labels": labels});
1149 }
1150
1151 if let Some(pod_labels) = pod_labels {
1154 patch["spec"]["template"]["metadata"] = json!({"labels": pod_labels});
1155 }
1156
1157 api.patch(name, &PatchParams::default(), &Patch::Strategic(&patch))
1158 .await?;
1159
1160 Ok(())
1161}
1162
1163async fn create_or_update_service(
1165 client: &Client,
1166 namespace: &str,
1167 name: &str,
1168 instance: &Bind9Instance,
1169 cluster: Option<&Bind9Cluster>,
1170 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1171) -> Result<()> {
1172 let custom_spec = cluster
1174 .and_then(|c| match instance.spec.role {
1175 crate::crd::ServerRole::Primary => c
1176 .spec
1177 .common
1178 .primary
1179 .as_ref()
1180 .and_then(|p| p.service.as_ref()),
1181 crate::crd::ServerRole::Secondary => c
1182 .spec
1183 .common
1184 .secondary
1185 .as_ref()
1186 .and_then(|s| s.service.as_ref()),
1187 })
1188 .or_else(|| {
1189 cluster_provider.and_then(|gc| match instance.spec.role {
1191 crate::crd::ServerRole::Primary => gc
1192 .spec
1193 .common
1194 .primary
1195 .as_ref()
1196 .and_then(|p| p.service.as_ref()),
1197 crate::crd::ServerRole::Secondary => gc
1198 .spec
1199 .common
1200 .secondary
1201 .as_ref()
1202 .and_then(|s| s.service.as_ref()),
1203 })
1204 });
1205
1206 let service = build_service(name, namespace, instance, custom_spec);
1207 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1208
1209 if let Ok(existing) = svc_api.get(name).await {
1210 info!("Updating Service {}/{}", namespace, name);
1212 let mut updated_service = service;
1213 if let Some(ref mut spec) = updated_service.spec {
1214 if let Some(ref existing_spec) = existing.spec {
1215 spec.cluster_ip.clone_from(&existing_spec.cluster_ip);
1216 spec.cluster_ips.clone_from(&existing_spec.cluster_ips);
1217 }
1218 }
1219 svc_api
1220 .replace(name, &PostParams::default(), &updated_service)
1221 .await?;
1222 } else {
1223 info!("Creating Service {}/{}", namespace, name);
1225 svc_api.create(&PostParams::default(), &service).await?;
1226 }
1227
1228 Ok(())
1229}
1230
1231pub async fn delete_bind9instance(ctx: Arc<Context>, instance: Bind9Instance) -> Result<()> {
1252 let namespace = instance.namespace().unwrap_or_default();
1253 let name = instance.name_any();
1254
1255 info!("Deleting Bind9Instance: {}/{}", namespace, name);
1256
1257 delete_resources(&ctx.client, &namespace, &name).await?;
1259
1260 info!("Successfully deleted resources for {}/{}", namespace, name);
1261
1262 Ok(())
1263}
1264
1265pub(super) async fn delete_resources(client: &Client, namespace: &str, name: &str) -> Result<()> {
1267 let delete_params = kube::api::DeleteParams::default();
1268
1269 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1271 match svc_api.delete(name, &delete_params).await {
1272 Ok(_) => info!("Deleted Service {}/{}", namespace, name),
1273 Err(e) => warn!("Failed to delete Service {}/{}: {}", namespace, name, e),
1274 }
1275
1276 let deploy_api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
1278 match deploy_api.delete(name, &delete_params).await {
1279 Ok(_) => info!("Deleted Deployment {}/{}", namespace, name),
1280 Err(e) => warn!("Failed to delete Deployment {}/{}: {}", namespace, name, e),
1281 }
1282
1283 let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
1285 let cm_name = format!("{name}-config");
1286 match cm_api.delete(&cm_name, &delete_params).await {
1287 Ok(_) => info!("Deleted ConfigMap {}/{}", namespace, cm_name),
1288 Err(e) => warn!(
1289 "Failed to delete ConfigMap {}/{}: {}",
1290 namespace, cm_name, e
1291 ),
1292 }
1293
1294 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
1296 let secret_name = format!("{name}-rndc-key");
1297 match secret_api.delete(&secret_name, &delete_params).await {
1298 Ok(_) => info!("Deleted Secret {}/{}", namespace, secret_name),
1299 Err(e) => warn!(
1300 "Failed to delete Secret {}/{}: {}",
1301 namespace, secret_name, e
1302 ),
1303 }
1304
1305 let sa_api: Api<ServiceAccount> = Api::namespaced(client.clone(), namespace);
1307 let sa_name = crate::constants::BIND9_SERVICE_ACCOUNT;
1308 match sa_api.get(sa_name).await {
1309 Ok(sa) => {
1310 let is_owner = sa
1312 .metadata
1313 .owner_references
1314 .as_ref()
1315 .is_some_and(|owners| owners.iter().any(|owner| owner.name == name));
1316
1317 if is_owner {
1318 match sa_api.delete(sa_name, &delete_params).await {
1319 Ok(_) => info!("Deleted ServiceAccount {}/{}", namespace, sa_name),
1320 Err(e) => warn!(
1321 "Failed to delete ServiceAccount {}/{}: {}",
1322 namespace, sa_name, e
1323 ),
1324 }
1325 } else {
1326 debug!(
1327 "ServiceAccount {}/{} is not owned by this instance, skipping deletion",
1328 namespace, sa_name
1329 );
1330 }
1331 }
1332 Err(e) => {
1333 debug!(
1334 "ServiceAccount {}/{} does not exist or cannot be retrieved: {}",
1335 namespace, sa_name, e
1336 );
1337 }
1338 }
1339
1340 Ok(())
1341}
1342
1343#[cfg(test)]
1349pub(super) fn validate_user_pod_shape_for_test(
1350 instance: &Bind9Instance,
1351 cluster: Option<&Bind9Cluster>,
1352 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1353) -> anyhow::Result<()> {
1354 validate_user_pod_shape(instance, cluster, cluster_provider)
1355}
1356
1357fn validate_user_pod_shape(
1371 instance: &Bind9Instance,
1372 cluster: Option<&Bind9Cluster>,
1373 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1374) -> anyhow::Result<()> {
1375 use crate::safe_volume::{
1376 validate_optional_user_volume_mounts, validate_optional_user_volumes,
1377 };
1378
1379 let instance_config = instance.spec.config.as_ref();
1385 let cluster_global = cluster.and_then(|c| c.spec.common.global.as_ref());
1386 let provider_global = cluster_provider.and_then(|p| p.spec.common.global.as_ref());
1387 for global in [cluster_global, provider_global] {
1388 let Some(signing) =
1389 crate::bind9_resources::get_dnssec_signing_config(global, instance_config)
1390 else {
1391 continue;
1392 };
1393 if let Some(secret) = signing
1394 .keys_from
1395 .as_ref()
1396 .and_then(|k| k.secret_ref.as_ref())
1397 {
1398 crate::safe_volume::validate_dnssec_key_secret_name(&secret.name).with_context(
1399 || {
1400 format!(
1401 "Bind9Instance {} spec.dnssec.signing.keysFrom.secretRef",
1402 instance.name_any()
1403 )
1404 },
1405 )?;
1406 }
1407 }
1408
1409 validate_optional_user_volumes(instance.spec.volumes.as_ref())
1411 .with_context(|| format!("Bind9Instance {} spec.volumes", instance.name_any()))?;
1412 validate_optional_user_volume_mounts(instance.spec.volume_mounts.as_ref())
1413 .with_context(|| format!("Bind9Instance {} spec.volumeMounts", instance.name_any()))?;
1414
1415 if let Some(c) = cluster {
1417 validate_optional_user_volumes(c.spec.common.volumes.as_ref()).with_context(|| {
1418 format!(
1419 "Bind9Cluster {}/{} spec.volumes",
1420 c.namespace().unwrap_or_default(),
1421 c.name_any(),
1422 )
1423 })?;
1424 validate_optional_user_volume_mounts(c.spec.common.volume_mounts.as_ref()).with_context(
1425 || {
1426 format!(
1427 "Bind9Cluster {}/{} spec.volumeMounts",
1428 c.namespace().unwrap_or_default(),
1429 c.name_any(),
1430 )
1431 },
1432 )?;
1433 }
1434 if let Some(p) = cluster_provider {
1435 validate_optional_user_volumes(p.spec.common.volumes.as_ref())
1436 .with_context(|| format!("ClusterBind9Provider {} spec.volumes", p.name_any()))?;
1437 validate_optional_user_volume_mounts(p.spec.common.volume_mounts.as_ref())
1438 .with_context(|| format!("ClusterBind9Provider {} spec.volumeMounts", p.name_any()))?;
1439 }
1440 Ok(())
1441}
1442
1443#[cfg(test)]
1444#[path = "resources_tests.rs"]
1445mod resources_tests;