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 let rndc_config =
158 resolve_full_rndc_config(instance, cluster.as_ref(), cluster_provider.as_ref());
159 debug!(
160 "Resolved RNDC config: auto_rotate={}, rotate_after={}",
161 rndc_config.auto_rotate, rndc_config.rotate_after
162 );
163
164 debug!("Step 1: Creating/updating ServiceAccount");
166 create_or_update_service_account(client, namespace, instance).await?;
167
168 debug!("Step 2: Creating/updating RNDC Secret with rotation support");
170 let secret_name =
171 create_or_update_rndc_secret_with_config(client, namespace, name, instance, &rndc_config)
172 .await?;
173
174 let secret = if rndc_config.auto_rotate {
176 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
177 secret_api.get(&secret_name).await.ok()
178 } else {
179 None
180 };
181
182 debug!("Step 3: Creating/updating ConfigMap");
184 create_or_update_configmap(
185 client,
186 namespace,
187 name,
188 instance,
189 cluster.as_ref(),
190 cluster_provider.as_ref(),
191 )
192 .await?;
193
194 debug!("Step 4: Creating/updating Deployment");
196 create_or_update_deployment(
197 client,
198 namespace,
199 name,
200 instance,
201 cluster.as_ref(),
202 cluster_provider.as_ref(),
203 )
204 .await?;
205
206 debug!("Step 5: Creating/updating Service");
208 create_or_update_service(
209 client,
210 namespace,
211 name,
212 instance,
213 cluster.as_ref(),
214 cluster_provider.as_ref(),
215 )
216 .await?;
217
218 debug!("Successfully created/updated all resources");
219 Ok((cluster, cluster_provider, secret))
220}
221
222async fn create_or_update_service_account(
224 client: &Client,
225 namespace: &str,
226 instance: &Bind9Instance,
227) -> Result<()> {
228 let service_account = build_service_account(namespace, instance);
229 create_or_apply(client, namespace, &service_account, "bindy-controller").await
230}
231
232#[allow(dead_code)] #[allow(clippy::too_many_lines)] async fn create_or_update_rndc_secret_with_config(
258 client: &Client,
259 namespace: &str,
260 name: &str,
261 instance: &Bind9Instance,
262 config: &crate::crd::RndcKeyConfig,
263) -> Result<String> {
264 use chrono::Utc;
265
266 if let Some(ref secret_ref) = config.secret_ref {
268 info!(
269 "Using existing Secret reference: {}/{}",
270 namespace, secret_ref.name
271 );
272 return Ok(secret_ref.name.clone());
273 }
274
275 let secret_name = if let Some(ref secret_spec) = config.secret {
277 secret_spec.metadata.name.clone()
279 } else {
280 format!("{name}-rndc-key")
282 };
283
284 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
285
286 match secret_api.get(&secret_name).await {
288 Ok(existing_secret) => {
289 if let Some(ref data) = existing_secret.data {
291 if !data.contains_key("key-name")
292 || !data.contains_key("algorithm")
293 || !data.contains_key("secret")
294 {
295 warn!(
296 "RNDC Secret {}/{} is missing required keys, will recreate",
297 namespace, secret_name
298 );
299 secret_api
300 .delete(&secret_name, &kube::api::DeleteParams::default())
301 .await?;
302 }
304 } else {
305 warn!(
306 "RNDC Secret {}/{} has no data, will recreate",
307 namespace, secret_name
308 );
309 secret_api
310 .delete(&secret_name, &kube::api::DeleteParams::default())
311 .await?;
312 }
314
315 let has_annotations = existing_secret
317 .metadata
318 .annotations
319 .as_ref()
320 .and_then(|a| a.get(crate::constants::ANNOTATION_RNDC_CREATED_AT))
321 .is_some();
322
323 if config.auto_rotate && !has_annotations {
324 info!(
325 "RNDC Secret {}/{} missing rotation annotations, adding them",
326 namespace, secret_name
327 );
328 add_rotation_annotations_to_secret(&secret_api, &secret_name, config).await?;
329 return Ok(secret_name);
330 }
331
332 if config.auto_rotate && should_rotate_secret(&existing_secret, config)? {
334 info!(
335 "RNDC Secret {}/{} rotation is due, rotating",
336 namespace, secret_name
337 );
338 rotate_rndc_secret(
339 client,
340 namespace,
341 &secret_name,
342 config,
343 instance,
344 &existing_secret,
345 )
346 .await?;
347 return Ok(secret_name);
348 }
349
350 if let Some(ref data) = existing_secret.data {
352 if data.contains_key("algorithm") {
353 let current_algorithm =
354 std::str::from_utf8(&data.get("algorithm").unwrap().0).unwrap_or("unknown");
355 let desired_algorithm = config.algorithm.as_str();
356
357 if current_algorithm == desired_algorithm {
358 info!(
359 "RNDC Secret {}/{} exists and is valid, skipping creation",
360 namespace, secret_name
361 );
362 return Ok(secret_name);
363 }
364 warn!(
365 "RNDC Secret {}/{} algorithm mismatch (current: {}, desired: {}), will recreate",
366 namespace, secret_name, current_algorithm, desired_algorithm
367 );
368 secret_api
369 .delete(&secret_name, &kube::api::DeleteParams::default())
370 .await?;
371 }
373 }
374 }
375 Err(_) => {
376 info!(
377 "RNDC Secret {}/{} does not exist, creating",
378 namespace, secret_name
379 );
380 }
381 }
382
383 if let Some(_secret_spec) = &config.secret {
385 info!("Creating RNDC Secret from inline spec with rotation enabled");
388 }
389
390 let mut key_data = Bind9Manager::generate_rndc_key();
392 key_data.name = "bindy-operator".to_string();
393 key_data.algorithm = config.algorithm.clone();
394
395 let created_at = Utc::now();
397 let rotate_after = if config.auto_rotate {
398 crate::bind9::duration::parse_duration(&config.rotate_after).ok()
399 } else {
400 None
401 };
402
403 let secret = crate::bind9::rndc::create_rndc_secret_with_annotations(
405 namespace,
406 &secret_name,
407 &key_data,
408 created_at,
409 rotate_after,
410 0, );
412
413 let owner_ref = OwnerReference {
415 api_version: API_GROUP_VERSION.to_string(),
416 kind: KIND_BIND9_INSTANCE.to_string(),
417 name: name.to_string(),
418 uid: instance.metadata.uid.clone().unwrap_or_default(),
419 controller: Some(true),
420 block_owner_deletion: Some(true),
421 };
422
423 let mut secret_with_owner = secret;
424 secret_with_owner
425 .metadata
426 .owner_references
427 .get_or_insert_with(Vec::new)
428 .push(owner_ref);
429
430 if config.auto_rotate {
432 info!(
433 "Creating RNDC Secret {}/{} with rotation enabled (rotate after: {})",
434 namespace, secret_name, config.rotate_after
435 );
436 } else {
437 info!(
438 "Creating RNDC Secret {}/{} without rotation",
439 namespace, secret_name
440 );
441 }
442
443 secret_api
444 .create(&PostParams::default(), &secret_with_owner)
445 .await?;
446
447 Ok(secret_name)
448}
449
450#[allow(dead_code)] async fn create_or_update_rndc_secret(
456 client: &Client,
457 namespace: &str,
458 name: &str,
459 instance: &Bind9Instance,
460) -> Result<()> {
461 let secret_name = format!("{name}-rndc-key");
462 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
463
464 match secret_api.get(&secret_name).await {
466 Ok(existing_secret) => {
467 info!(
469 "RNDC Secret {}/{} already exists, skipping",
470 namespace, secret_name
471 );
472 if let Some(ref data) = existing_secret.data {
474 if !data.contains_key("key-name")
475 || !data.contains_key("algorithm")
476 || !data.contains_key("secret")
477 {
478 warn!(
479 "RNDC Secret {}/{} is missing required keys, will recreate",
480 namespace, secret_name
481 );
482 secret_api
484 .delete(&secret_name, &kube::api::DeleteParams::default())
485 .await?;
486 } else {
487 return Ok(());
488 }
489 } else {
490 warn!(
491 "RNDC Secret {}/{} has no data, will recreate",
492 namespace, secret_name
493 );
494 secret_api
495 .delete(&secret_name, &kube::api::DeleteParams::default())
496 .await?;
497 }
498 }
499 Err(_) => {
500 info!(
501 "RNDC Secret {}/{} does not exist, creating",
502 namespace, secret_name
503 );
504 }
505 }
506
507 let mut key_data = Bind9Manager::generate_rndc_key();
509 key_data.name = "bindy-operator".to_string();
510
511 let secret_data = Bind9Manager::create_rndc_secret_data(&key_data);
513
514 let owner_ref = OwnerReference {
516 api_version: API_GROUP_VERSION.to_string(),
517 kind: KIND_BIND9_INSTANCE.to_string(),
518 name: name.to_string(),
519 uid: instance.metadata.uid.clone().unwrap_or_default(),
520 controller: Some(true),
521 block_owner_deletion: Some(true),
522 };
523
524 let secret = Secret {
526 metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
527 name: Some(secret_name.clone()),
528 namespace: Some(namespace.to_string()),
529 owner_references: Some(vec![owner_ref]),
530 ..Default::default()
531 },
532 string_data: Some(secret_data),
533 ..Default::default()
534 };
535
536 info!("Creating RNDC Secret {}/{}", namespace, secret_name);
538 secret_api.create(&PostParams::default(), &secret).await?;
539
540 Ok(())
541}
542
543async fn add_rotation_annotations_to_secret(
557 secret_api: &Api<Secret>,
558 secret_name: &str,
559 config: &crate::crd::RndcKeyConfig,
560) -> Result<()> {
561 use chrono::Utc;
562 use kube::api::{Patch, PatchParams};
563 use std::collections::BTreeMap;
564
565 let created_at = Utc::now();
566 let rotate_after = crate::bind9::duration::parse_duration(&config.rotate_after)?;
567 let rotate_at = created_at + chrono::Duration::from_std(rotate_after)?;
568
569 let mut annotations = BTreeMap::new();
570 annotations.insert(
571 crate::constants::ANNOTATION_RNDC_CREATED_AT.to_string(),
572 created_at.to_rfc3339(),
573 );
574 annotations.insert(
575 crate::constants::ANNOTATION_RNDC_ROTATE_AT.to_string(),
576 rotate_at.to_rfc3339(),
577 );
578 annotations.insert(
579 crate::constants::ANNOTATION_RNDC_ROTATION_COUNT.to_string(),
580 "0".to_string(),
581 );
582
583 let patch = serde_json::json!({
584 "metadata": {
585 "annotations": annotations
586 }
587 });
588
589 info!(
590 "Adding rotation annotations to existing Secret {} (rotate at: {})",
591 secret_name,
592 rotate_at.to_rfc3339()
593 );
594
595 secret_api
596 .patch(
597 secret_name,
598 &PatchParams::apply("bindy-operator"),
599 &Patch::Merge(&patch),
600 )
601 .await?;
602
603 Ok(())
604}
605
606pub(super) fn should_rotate_secret(
627 secret: &Secret,
628 config: &crate::crd::RndcKeyConfig,
629) -> Result<bool> {
630 use chrono::Utc;
631
632 if !config.auto_rotate {
634 return Ok(false);
635 }
636
637 let Some(annotations) = &secret.metadata.annotations else {
639 debug!("Secret has no annotations, rotation not due");
640 return Ok(false);
641 };
642
643 let (created_at, rotate_at, _rotation_count) =
644 crate::bind9::rndc::parse_rotation_annotations(annotations)?;
645
646 let now = Utc::now();
647
648 let time_since_creation = now.signed_duration_since(created_at);
650 if time_since_creation.num_hours() < crate::constants::MIN_TIME_BETWEEN_ROTATIONS_HOURS {
651 debug!(
652 "Skipping rotation - Secret was created/rotated {} minutes ago (min 1 hour required)",
653 time_since_creation.num_minutes()
654 );
655 return Ok(false);
656 }
657
658 Ok(crate::bind9::rndc::is_rotation_due(rotate_at, now))
660}
661
662#[allow(dead_code)] async fn rotate_rndc_secret(
686 client: &Client,
687 namespace: &str,
688 secret_name: &str,
689 config: &crate::crd::RndcKeyConfig,
690 instance: &Bind9Instance,
691 existing_secret: &Secret,
692) -> Result<()> {
693 use chrono::Utc;
694
695 let annotations = existing_secret
697 .metadata
698 .annotations
699 .as_ref()
700 .context("Secret missing annotations")?;
701
702 let (_created_at, _rotate_at, rotation_count) =
703 crate::bind9::rndc::parse_rotation_annotations(annotations)?;
704
705 let new_rotation_count = rotation_count + 1;
707
708 info!(
709 "Rotating RNDC Secret {}/{} (rotation #{})",
710 namespace, secret_name, new_rotation_count
711 );
712
713 let mut key_data = Bind9Manager::generate_rndc_key();
715 key_data.name = "bindy-operator".to_string();
716 key_data.algorithm = config.algorithm.clone();
717
718 let created_at = Utc::now();
720 let rotate_after = crate::bind9::duration::parse_duration(&config.rotate_after)?;
721
722 let new_secret = crate::bind9::rndc::create_rndc_secret_with_annotations(
724 namespace,
725 secret_name,
726 &key_data,
727 created_at,
728 Some(rotate_after),
729 new_rotation_count,
730 );
731
732 let mut updated_secret = new_secret;
734 updated_secret
735 .metadata
736 .owner_references
737 .clone_from(&existing_secret.metadata.owner_references);
738
739 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
741 secret_api
742 .replace(secret_name, &PostParams::default(), &updated_secret)
743 .await?;
744
745 info!(
746 "Successfully rotated RNDC Secret {}/{} (rotation #{})",
747 namespace, secret_name, new_rotation_count
748 );
749
750 trigger_deployment_rollout(client, namespace, &instance.name_any()).await?;
752
753 Ok(())
754}
755
756async fn trigger_deployment_rollout(
768 client: &Client,
769 namespace: &str,
770 instance_name: &str,
771) -> Result<()> {
772 use chrono::Utc;
773 use serde_json::json;
774
775 let deployment_api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
776
777 let patch = json!({
779 "spec": {
780 "template": {
781 "metadata": {
782 "annotations": {
783 crate::constants::ANNOTATION_RNDC_ROTATED_AT: Utc::now().to_rfc3339()
784 }
785 }
786 }
787 }
788 });
789
790 deployment_api
791 .patch(
792 instance_name,
793 &PatchParams::default(),
794 &kube::api::Patch::Merge(&patch),
795 )
796 .await?;
797
798 info!(
799 "Triggered Deployment {}/{} rollout after RNDC rotation",
800 namespace, instance_name
801 );
802
803 Ok(())
804}
805
806async fn create_or_update_configmap(
812 client: &Client,
813 namespace: &str,
814 name: &str,
815 instance: &Bind9Instance,
816 cluster: Option<&Bind9Cluster>,
817 _cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
818) -> Result<()> {
819 if !instance.spec.cluster_ref.is_empty() {
822 debug!(
823 "Instance {}/{} belongs to cluster '{}', using cluster ConfigMap",
824 namespace, name, instance.spec.cluster_ref
825 );
826 return Ok(());
827 }
828
829 info!(
831 "Instance {}/{} is standalone, creating instance-specific ConfigMap",
832 namespace, name
833 );
834
835 let role_allow_transfer = cluster.and_then(|c| match instance.spec.role {
839 crate::crd::ServerRole::Primary => c
840 .spec
841 .common
842 .primary
843 .as_ref()
844 .and_then(|p| p.allow_transfer.as_ref()),
845 crate::crd::ServerRole::Secondary => c
846 .spec
847 .common
848 .secondary
849 .as_ref()
850 .and_then(|s| s.allow_transfer.as_ref()),
851 });
852
853 if let Some(configmap) =
855 build_configmap(name, namespace, instance, cluster, role_allow_transfer)
856 {
857 let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
858 let cm_name = format!("{name}-config");
859
860 if (cm_api.get(&cm_name).await).is_ok() {
861 info!("Updating ConfigMap {}/{}", namespace, cm_name);
863 cm_api
864 .replace(&cm_name, &PostParams::default(), &configmap)
865 .await?;
866 } else {
867 info!("Creating ConfigMap {}/{}", namespace, cm_name);
869 cm_api.create(&PostParams::default(), &configmap).await?;
870 }
871 } else {
872 info!(
873 "Using custom ConfigMaps for {}/{}, skipping ConfigMap creation",
874 namespace, name
875 );
876 }
877
878 Ok(())
879}
880
881fn deployment_needs_update(current: &Deployment, desired: &Deployment) -> bool {
890 let desired_replicas = desired.spec.as_ref().and_then(|s| s.replicas);
892 let current_replicas = current.spec.as_ref().and_then(|s| s.replicas);
893
894 if desired_replicas != current_replicas {
895 debug!(
896 "Replicas changed: current={:?}, desired={:?}",
897 current_replicas, desired_replicas
898 );
899 return true;
900 }
901
902 let current_api_container = current
904 .spec
905 .as_ref()
906 .and_then(|s| s.template.spec.as_ref())
907 .and_then(|pod_spec| {
908 pod_spec
909 .containers
910 .iter()
911 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
912 });
913
914 let desired_api_container = desired
916 .spec
917 .as_ref()
918 .and_then(|s| s.template.spec.as_ref())
919 .and_then(|pod_spec| {
920 pod_spec
921 .containers
922 .iter()
923 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
924 });
925
926 if let (Some(current_api), Some(desired_api)) = (current_api_container, desired_api_container) {
928 if current_api.image != desired_api.image {
930 debug!(
931 "API container image changed: current={:?}, desired={:?}",
932 current_api.image, desired_api.image
933 );
934 return true;
935 }
936
937 if current_api.env != desired_api.env {
939 debug!("API container env changed");
940 return true;
941 }
942
943 if current_api.image_pull_policy != desired_api.image_pull_policy {
945 debug!(
946 "API container imagePullPolicy changed: current={:?}, desired={:?}",
947 current_api.image_pull_policy, desired_api.image_pull_policy
948 );
949 return true;
950 }
951
952 if current_api.resources != desired_api.resources {
954 debug!("API container resources changed");
955 return true;
956 }
957 } else if current_api_container.is_some() != desired_api_container.is_some() {
958 debug!("API container existence changed");
960 return true;
961 }
962
963 false
964}
965
966async fn create_or_update_deployment(
968 client: &Client,
969 namespace: &str,
970 name: &str,
971 instance: &Bind9Instance,
972 cluster: Option<&Bind9Cluster>,
973 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
974) -> Result<()> {
975 let deployment = build_deployment(name, namespace, instance, cluster, cluster_provider);
976 let api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
977
978 if api.get(name).await.is_err() {
980 info!("Creating Deployment {}/{}", namespace, name);
981 api.create(&PostParams::default(), &deployment).await?;
982 return Ok(());
983 }
984
985 debug!(
987 "Checking if Deployment {}/{} needs updating",
988 namespace, name
989 );
990
991 let current_deployment = api.get(name).await?;
993
994 if !deployment_needs_update(¤t_deployment, &deployment) {
996 debug!(
997 "Deployment {}/{} is up to date, skipping patch",
998 namespace, name
999 );
1000 return Ok(());
1001 }
1002
1003 info!("Patching Deployment {}/{}", namespace, name);
1005
1006 let api_container = deployment
1007 .spec
1008 .as_ref()
1009 .and_then(|s| s.template.spec.as_ref())
1010 .and_then(|pod_spec| {
1011 pod_spec
1012 .containers
1013 .iter()
1014 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
1015 });
1016
1017 let mut patch_containers = vec![];
1018
1019 patch_containers.push(json!({
1021 "name": crate::constants::CONTAINER_NAME_BIND9
1022 }));
1023
1024 if let Some(api) = api_container {
1026 let mut api_patch = json!({
1027 "name": crate::constants::CONTAINER_NAME_BINDCAR
1028 });
1029
1030 if let Some(ref image) = api.image {
1032 api_patch["image"] = json!(image);
1033 }
1034
1035 if let Some(ref env) = api.env {
1037 api_patch["env"] = json!(env);
1038 }
1039
1040 if let Some(ref pull_policy) = api.image_pull_policy {
1042 api_patch["imagePullPolicy"] = json!(pull_policy);
1043 }
1044
1045 if let Some(ref resources) = api.resources {
1047 api_patch["resources"] = json!(resources);
1048 }
1049
1050 patch_containers.push(api_patch);
1051 }
1052
1053 let labels = deployment.metadata.labels.as_ref();
1055 let pod_labels = deployment
1056 .spec
1057 .as_ref()
1058 .and_then(|s| s.template.metadata.as_ref())
1059 .and_then(|m| m.labels.as_ref());
1060
1061 let mut patch = json!({
1065 "spec": {
1066 "replicas": deployment.spec.as_ref().and_then(|s| s.replicas),
1067 "template": {
1068 "spec": {
1069 "containers": patch_containers,
1070 "$setElementOrder/containers": [
1071 {"name": crate::constants::CONTAINER_NAME_BIND9},
1072 {"name": crate::constants::CONTAINER_NAME_BINDCAR}
1073 ]
1074 }
1075 }
1076 }
1077 });
1078
1079 if let Some(labels) = labels {
1083 patch["metadata"] = json!({"labels": labels});
1084 }
1085
1086 if let Some(pod_labels) = pod_labels {
1089 patch["spec"]["template"]["metadata"] = json!({"labels": pod_labels});
1090 }
1091
1092 api.patch(name, &PatchParams::default(), &Patch::Strategic(&patch))
1093 .await?;
1094
1095 Ok(())
1096}
1097
1098async fn create_or_update_service(
1100 client: &Client,
1101 namespace: &str,
1102 name: &str,
1103 instance: &Bind9Instance,
1104 cluster: Option<&Bind9Cluster>,
1105 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1106) -> Result<()> {
1107 let custom_spec = cluster
1109 .and_then(|c| match instance.spec.role {
1110 crate::crd::ServerRole::Primary => c
1111 .spec
1112 .common
1113 .primary
1114 .as_ref()
1115 .and_then(|p| p.service.as_ref()),
1116 crate::crd::ServerRole::Secondary => c
1117 .spec
1118 .common
1119 .secondary
1120 .as_ref()
1121 .and_then(|s| s.service.as_ref()),
1122 })
1123 .or_else(|| {
1124 cluster_provider.and_then(|gc| match instance.spec.role {
1126 crate::crd::ServerRole::Primary => gc
1127 .spec
1128 .common
1129 .primary
1130 .as_ref()
1131 .and_then(|p| p.service.as_ref()),
1132 crate::crd::ServerRole::Secondary => gc
1133 .spec
1134 .common
1135 .secondary
1136 .as_ref()
1137 .and_then(|s| s.service.as_ref()),
1138 })
1139 });
1140
1141 let service = build_service(name, namespace, instance, custom_spec);
1142 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1143
1144 if let Ok(existing) = svc_api.get(name).await {
1145 info!("Updating Service {}/{}", namespace, name);
1147 let mut updated_service = service;
1148 if let Some(ref mut spec) = updated_service.spec {
1149 if let Some(ref existing_spec) = existing.spec {
1150 spec.cluster_ip.clone_from(&existing_spec.cluster_ip);
1151 spec.cluster_ips.clone_from(&existing_spec.cluster_ips);
1152 }
1153 }
1154 svc_api
1155 .replace(name, &PostParams::default(), &updated_service)
1156 .await?;
1157 } else {
1158 info!("Creating Service {}/{}", namespace, name);
1160 svc_api.create(&PostParams::default(), &service).await?;
1161 }
1162
1163 Ok(())
1164}
1165
1166pub async fn delete_bind9instance(ctx: Arc<Context>, instance: Bind9Instance) -> Result<()> {
1187 let namespace = instance.namespace().unwrap_or_default();
1188 let name = instance.name_any();
1189
1190 info!("Deleting Bind9Instance: {}/{}", namespace, name);
1191
1192 delete_resources(&ctx.client, &namespace, &name).await?;
1194
1195 info!("Successfully deleted resources for {}/{}", namespace, name);
1196
1197 Ok(())
1198}
1199
1200pub(super) async fn delete_resources(client: &Client, namespace: &str, name: &str) -> Result<()> {
1202 let delete_params = kube::api::DeleteParams::default();
1203
1204 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1206 match svc_api.delete(name, &delete_params).await {
1207 Ok(_) => info!("Deleted Service {}/{}", namespace, name),
1208 Err(e) => warn!("Failed to delete Service {}/{}: {}", namespace, name, e),
1209 }
1210
1211 let deploy_api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
1213 match deploy_api.delete(name, &delete_params).await {
1214 Ok(_) => info!("Deleted Deployment {}/{}", namespace, name),
1215 Err(e) => warn!("Failed to delete Deployment {}/{}: {}", namespace, name, e),
1216 }
1217
1218 let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
1220 let cm_name = format!("{name}-config");
1221 match cm_api.delete(&cm_name, &delete_params).await {
1222 Ok(_) => info!("Deleted ConfigMap {}/{}", namespace, cm_name),
1223 Err(e) => warn!(
1224 "Failed to delete ConfigMap {}/{}: {}",
1225 namespace, cm_name, e
1226 ),
1227 }
1228
1229 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
1231 let secret_name = format!("{name}-rndc-key");
1232 match secret_api.delete(&secret_name, &delete_params).await {
1233 Ok(_) => info!("Deleted Secret {}/{}", namespace, secret_name),
1234 Err(e) => warn!(
1235 "Failed to delete Secret {}/{}: {}",
1236 namespace, secret_name, e
1237 ),
1238 }
1239
1240 let sa_api: Api<ServiceAccount> = Api::namespaced(client.clone(), namespace);
1242 let sa_name = crate::constants::BIND9_SERVICE_ACCOUNT;
1243 match sa_api.get(sa_name).await {
1244 Ok(sa) => {
1245 let is_owner = sa
1247 .metadata
1248 .owner_references
1249 .as_ref()
1250 .is_some_and(|owners| owners.iter().any(|owner| owner.name == name));
1251
1252 if is_owner {
1253 match sa_api.delete(sa_name, &delete_params).await {
1254 Ok(_) => info!("Deleted ServiceAccount {}/{}", namespace, sa_name),
1255 Err(e) => warn!(
1256 "Failed to delete ServiceAccount {}/{}: {}",
1257 namespace, sa_name, e
1258 ),
1259 }
1260 } else {
1261 debug!(
1262 "ServiceAccount {}/{} is not owned by this instance, skipping deletion",
1263 namespace, sa_name
1264 );
1265 }
1266 }
1267 Err(e) => {
1268 debug!(
1269 "ServiceAccount {}/{} does not exist or cannot be retrieved: {}",
1270 namespace, sa_name, e
1271 );
1272 }
1273 }
1274
1275 Ok(())
1276}
1277
1278#[cfg(test)]
1279#[path = "resources_tests.rs"]
1280mod resources_tests;