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,
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 let current_pod = current.spec.as_ref().and_then(|s| s.template.spec.as_ref());
1020 let desired_pod = desired.spec.as_ref().and_then(|s| s.template.spec.as_ref());
1021
1022 if current_pod.and_then(|p| p.topology_spread_constraints.as_ref())
1023 != desired_pod.and_then(|p| p.topology_spread_constraints.as_ref())
1024 {
1025 debug!("Pod topologySpreadConstraints changed");
1026 return true;
1027 }
1028
1029 let current_pod_labels = current
1033 .spec
1034 .as_ref()
1035 .and_then(|s| s.template.metadata.as_ref())
1036 .and_then(|m| m.labels.as_ref());
1037 let desired_pod_labels = desired
1038 .spec
1039 .as_ref()
1040 .and_then(|s| s.template.metadata.as_ref())
1041 .and_then(|m| m.labels.as_ref());
1042 if current_pod_labels != desired_pod_labels {
1043 debug!("Pod template labels changed");
1044 return true;
1045 }
1046
1047 false
1048}
1049
1050fn build_placement_patch(desired: &Deployment) -> serde_json::Value {
1057 let desired_pod = desired.spec.as_ref().and_then(|s| s.template.spec.as_ref());
1058
1059 let constraints = match desired_pod.and_then(|p| p.topology_spread_constraints.as_ref()) {
1060 Some(list) => {
1061 let mut items = vec![json!({"$patch": "replace"})];
1062 items.extend(list.iter().map(|c| json!(c)));
1063 json!(items)
1064 }
1065 None => json!(null),
1066 };
1067
1068 json!({ "topologySpreadConstraints": constraints })
1069}
1070
1071#[allow(clippy::too_many_arguments)]
1078async fn create_or_update_deployment(
1079 client: &Client,
1080 namespace: &str,
1081 name: &str,
1082 instance: &Bind9Instance,
1083 cluster: Option<&Bind9Cluster>,
1084 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1085 rndc_secret_name: &str,
1086) -> Result<()> {
1087 let deployment = build_deployment(
1088 name,
1089 namespace,
1090 instance,
1091 cluster,
1092 cluster_provider,
1093 rndc_secret_name,
1094 );
1095 let api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
1096
1097 if api.get(name).await.is_err() {
1099 info!("Creating Deployment {}/{}", namespace, name);
1100 api.create(&PostParams::default(), &deployment).await?;
1101 return Ok(());
1102 }
1103
1104 debug!(
1106 "Checking if Deployment {}/{} needs updating",
1107 namespace, name
1108 );
1109
1110 let current_deployment = api.get(name).await?;
1112
1113 if !deployment_needs_update(¤t_deployment, &deployment) {
1115 debug!(
1116 "Deployment {}/{} is up to date, skipping patch",
1117 namespace, name
1118 );
1119 return Ok(());
1120 }
1121
1122 info!("Patching Deployment {}/{}", namespace, name);
1124
1125 let api_container = deployment
1126 .spec
1127 .as_ref()
1128 .and_then(|s| s.template.spec.as_ref())
1129 .and_then(|pod_spec| {
1130 pod_spec
1131 .containers
1132 .iter()
1133 .find(|c| c.name == crate::constants::CONTAINER_NAME_BINDCAR)
1134 });
1135
1136 let mut patch_containers = vec![];
1137
1138 patch_containers.push(json!({
1140 "name": crate::constants::CONTAINER_NAME_BIND9
1141 }));
1142
1143 if let Some(api) = api_container {
1145 let mut api_patch = json!({
1146 "name": crate::constants::CONTAINER_NAME_BINDCAR
1147 });
1148
1149 if let Some(ref image) = api.image {
1151 api_patch["image"] = json!(image);
1152 }
1153
1154 if let Some(ref env) = api.env {
1156 api_patch["env"] = json!(env);
1157 }
1158
1159 if let Some(ref pull_policy) = api.image_pull_policy {
1161 api_patch["imagePullPolicy"] = json!(pull_policy);
1162 }
1163
1164 if let Some(ref resources) = api.resources {
1166 api_patch["resources"] = json!(resources);
1167 }
1168
1169 patch_containers.push(api_patch);
1170 }
1171
1172 let labels = deployment.metadata.labels.as_ref();
1174 let pod_labels = deployment
1175 .spec
1176 .as_ref()
1177 .and_then(|s| s.template.metadata.as_ref())
1178 .and_then(|m| m.labels.as_ref());
1179
1180 let mut patch = json!({
1184 "spec": {
1185 "replicas": deployment.spec.as_ref().and_then(|s| s.replicas),
1186 "template": {
1187 "spec": {
1188 "containers": patch_containers,
1189 "$setElementOrder/containers": [
1190 {"name": crate::constants::CONTAINER_NAME_BIND9},
1191 {"name": crate::constants::CONTAINER_NAME_BINDCAR}
1192 ]
1193 }
1194 }
1195 }
1196 });
1197
1198 if let Some(pod_spec) = patch["spec"]["template"]["spec"].as_object_mut() {
1200 if let Some(placement) = build_placement_patch(&deployment).as_object() {
1201 for (key, value) in placement {
1202 pod_spec.insert(key.clone(), value.clone());
1203 }
1204 }
1205 }
1206
1207 if let Some(labels) = labels {
1211 patch["metadata"] = json!({"labels": labels});
1212 }
1213
1214 if let Some(pod_labels) = pod_labels {
1217 patch["spec"]["template"]["metadata"] = json!({"labels": pod_labels});
1218 }
1219
1220 api.patch(name, &PatchParams::default(), &Patch::Strategic(&patch))
1221 .await?;
1222
1223 Ok(())
1224}
1225
1226async fn create_or_update_service(
1228 client: &Client,
1229 namespace: &str,
1230 name: &str,
1231 instance: &Bind9Instance,
1232 cluster: Option<&Bind9Cluster>,
1233 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1234) -> Result<()> {
1235 let custom_spec = cluster
1237 .and_then(|c| match instance.spec.role {
1238 crate::crd::ServerRole::Primary => c
1239 .spec
1240 .common
1241 .primary
1242 .as_ref()
1243 .and_then(|p| p.service.as_ref()),
1244 crate::crd::ServerRole::Secondary => c
1245 .spec
1246 .common
1247 .secondary
1248 .as_ref()
1249 .and_then(|s| s.service.as_ref()),
1250 })
1251 .or_else(|| {
1252 cluster_provider.and_then(|gc| match instance.spec.role {
1254 crate::crd::ServerRole::Primary => gc
1255 .spec
1256 .common
1257 .primary
1258 .as_ref()
1259 .and_then(|p| p.service.as_ref()),
1260 crate::crd::ServerRole::Secondary => gc
1261 .spec
1262 .common
1263 .secondary
1264 .as_ref()
1265 .and_then(|s| s.service.as_ref()),
1266 })
1267 });
1268
1269 let service = build_service(name, namespace, instance, custom_spec);
1270 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1271
1272 if let Ok(existing) = svc_api.get(name).await {
1273 info!("Updating Service {}/{}", namespace, name);
1275 let mut updated_service = service;
1276 if let Some(ref mut spec) = updated_service.spec {
1277 if let Some(ref existing_spec) = existing.spec {
1278 spec.cluster_ip.clone_from(&existing_spec.cluster_ip);
1279 spec.cluster_ips.clone_from(&existing_spec.cluster_ips);
1280 }
1281 }
1282 svc_api
1283 .replace(name, &PostParams::default(), &updated_service)
1284 .await?;
1285 } else {
1286 info!("Creating Service {}/{}", namespace, name);
1288 svc_api.create(&PostParams::default(), &service).await?;
1289 }
1290
1291 Ok(())
1292}
1293
1294pub async fn delete_bind9instance(ctx: Arc<Context>, instance: Bind9Instance) -> Result<()> {
1315 let namespace = instance.namespace().unwrap_or_default();
1316 let name = instance.name_any();
1317
1318 info!("Deleting Bind9Instance: {}/{}", namespace, name);
1319
1320 delete_resources(&ctx.client, &namespace, &name).await?;
1322
1323 info!("Successfully deleted resources for {}/{}", namespace, name);
1324
1325 Ok(())
1326}
1327
1328pub(super) async fn delete_resources(client: &Client, namespace: &str, name: &str) -> Result<()> {
1330 let delete_params = kube::api::DeleteParams::default();
1331
1332 let svc_api: Api<Service> = Api::namespaced(client.clone(), namespace);
1334 match svc_api.delete(name, &delete_params).await {
1335 Ok(_) => info!("Deleted Service {}/{}", namespace, name),
1336 Err(e) => warn!("Failed to delete Service {}/{}: {}", namespace, name, e),
1337 }
1338
1339 let deploy_api: Api<Deployment> = Api::namespaced(client.clone(), namespace);
1341 match deploy_api.delete(name, &delete_params).await {
1342 Ok(_) => info!("Deleted Deployment {}/{}", namespace, name),
1343 Err(e) => warn!("Failed to delete Deployment {}/{}: {}", namespace, name, e),
1344 }
1345
1346 let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
1348 let cm_name = format!("{name}-config");
1349 match cm_api.delete(&cm_name, &delete_params).await {
1350 Ok(_) => info!("Deleted ConfigMap {}/{}", namespace, cm_name),
1351 Err(e) => warn!(
1352 "Failed to delete ConfigMap {}/{}: {}",
1353 namespace, cm_name, e
1354 ),
1355 }
1356
1357 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
1359 let secret_name = format!("{name}-rndc-key");
1360 match secret_api.delete(&secret_name, &delete_params).await {
1361 Ok(_) => info!("Deleted Secret {}/{}", namespace, secret_name),
1362 Err(e) => warn!(
1363 "Failed to delete Secret {}/{}: {}",
1364 namespace, secret_name, e
1365 ),
1366 }
1367
1368 let sa_api: Api<ServiceAccount> = Api::namespaced(client.clone(), namespace);
1370 let sa_name = crate::constants::BIND9_SERVICE_ACCOUNT;
1371 match sa_api.get(sa_name).await {
1372 Ok(sa) => {
1373 let is_owner = sa
1375 .metadata
1376 .owner_references
1377 .as_ref()
1378 .is_some_and(|owners| owners.iter().any(|owner| owner.name == name));
1379
1380 if is_owner {
1381 match sa_api.delete(sa_name, &delete_params).await {
1382 Ok(_) => info!("Deleted ServiceAccount {}/{}", namespace, sa_name),
1383 Err(e) => warn!(
1384 "Failed to delete ServiceAccount {}/{}: {}",
1385 namespace, sa_name, e
1386 ),
1387 }
1388 } else {
1389 debug!(
1390 "ServiceAccount {}/{} is not owned by this instance, skipping deletion",
1391 namespace, sa_name
1392 );
1393 }
1394 }
1395 Err(e) => {
1396 debug!(
1397 "ServiceAccount {}/{} does not exist or cannot be retrieved: {}",
1398 namespace, sa_name, e
1399 );
1400 }
1401 }
1402
1403 Ok(())
1404}
1405
1406#[cfg(test)]
1413pub(super) fn deployment_needs_update_for_test(current: &Deployment, desired: &Deployment) -> bool {
1414 deployment_needs_update(current, desired)
1415}
1416
1417#[cfg(test)]
1418pub(super) fn build_placement_patch_for_test(desired: &Deployment) -> serde_json::Value {
1419 build_placement_patch(desired)
1420}
1421
1422#[cfg(test)]
1428pub(super) fn validate_user_pod_shape_for_test(
1429 instance: &Bind9Instance,
1430 cluster: Option<&Bind9Cluster>,
1431 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1432) -> anyhow::Result<()> {
1433 validate_user_pod_shape(instance, cluster, cluster_provider)
1434}
1435
1436fn validate_user_pod_shape(
1450 instance: &Bind9Instance,
1451 cluster: Option<&Bind9Cluster>,
1452 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1453) -> anyhow::Result<()> {
1454 use crate::safe_volume::{
1455 validate_optional_user_volume_mounts, validate_optional_user_volumes,
1456 };
1457
1458 let instance_config = instance.spec.config.as_ref();
1464 let cluster_global = cluster.and_then(|c| c.spec.common.global.as_ref());
1465 let provider_global = cluster_provider.and_then(|p| p.spec.common.global.as_ref());
1466 for global in [cluster_global, provider_global] {
1467 let Some(signing) =
1468 crate::bind9_resources::get_dnssec_signing_config(global, instance_config)
1469 else {
1470 continue;
1471 };
1472 if let Some(secret) = signing
1473 .keys_from
1474 .as_ref()
1475 .and_then(|k| k.secret_ref.as_ref())
1476 {
1477 crate::safe_volume::validate_dnssec_key_secret_name(&secret.name).with_context(
1478 || {
1479 format!(
1480 "Bind9Instance {} spec.dnssec.signing.keysFrom.secretRef",
1481 instance.name_any()
1482 )
1483 },
1484 )?;
1485 }
1486 }
1487
1488 crate::placement::validate_optional_placement(instance.spec.placement.as_ref())
1495 .with_context(|| format!("Bind9Instance {} spec.placement", instance.name_any()))?;
1496 for (label, common) in [
1497 (
1498 cluster.map(|c| format!("Bind9Cluster {}", c.name_any())),
1499 cluster.map(|c| &c.spec.common),
1500 ),
1501 (
1502 cluster_provider.map(|p| format!("ClusterBind9Provider {}", p.name_any())),
1503 cluster_provider.map(|p| &p.spec.common),
1504 ),
1505 ] {
1506 let (Some(label), Some(common)) = (label, common) else {
1507 continue;
1508 };
1509 crate::placement::validate_optional_placement(
1510 common.primary.as_ref().and_then(|p| p.placement.as_ref()),
1511 )
1512 .with_context(|| format!("{label} spec.primary.placement"))?;
1513 crate::placement::validate_optional_placement(
1514 common.secondary.as_ref().and_then(|s| s.placement.as_ref()),
1515 )
1516 .with_context(|| format!("{label} spec.secondary.placement"))?;
1517 }
1518
1519 validate_optional_user_volumes(instance.spec.volumes.as_ref())
1521 .with_context(|| format!("Bind9Instance {} spec.volumes", instance.name_any()))?;
1522 validate_optional_user_volume_mounts(instance.spec.volume_mounts.as_ref())
1523 .with_context(|| format!("Bind9Instance {} spec.volumeMounts", instance.name_any()))?;
1524
1525 if let Some(c) = cluster {
1527 validate_optional_user_volumes(c.spec.common.volumes.as_ref()).with_context(|| {
1528 format!(
1529 "Bind9Cluster {}/{} spec.volumes",
1530 c.namespace().unwrap_or_default(),
1531 c.name_any(),
1532 )
1533 })?;
1534 validate_optional_user_volume_mounts(c.spec.common.volume_mounts.as_ref()).with_context(
1535 || {
1536 format!(
1537 "Bind9Cluster {}/{} spec.volumeMounts",
1538 c.namespace().unwrap_or_default(),
1539 c.name_any(),
1540 )
1541 },
1542 )?;
1543 }
1544 if let Some(p) = cluster_provider {
1545 validate_optional_user_volumes(p.spec.common.volumes.as_ref())
1546 .with_context(|| format!("ClusterBind9Provider {} spec.volumes", p.name_any()))?;
1547 validate_optional_user_volume_mounts(p.spec.common.volume_mounts.as_ref())
1548 .with_context(|| format!("ClusterBind9Provider {} spec.volumeMounts", p.name_any()))?;
1549 }
1550 Ok(())
1551}
1552
1553#[cfg(test)]
1554#[path = "resources_tests.rs"]
1555mod resources_tests;