1use crate::constants::{
10 API_GROUP_VERSION, BIND9_MALLOC_CONF, BIND9_NONROOT_UID, BIND9_SERVICE_ACCOUNT,
11 CONTAINER_NAME_BIND9, CONTAINER_NAME_BINDCAR, DEFAULT_BIND9_VERSION, DNS_CONTAINER_PORT,
12 DNS_PORT, KIND_BIND9_INSTANCE, LIVENESS_FAILURE_THRESHOLD, LIVENESS_INITIAL_DELAY_SECS,
13 LIVENESS_PERIOD_SECS, LIVENESS_TIMEOUT_SECS, READINESS_FAILURE_THRESHOLD,
14 READINESS_INITIAL_DELAY_SECS, READINESS_PERIOD_SECS, READINESS_TIMEOUT_SECS, RNDC_PORT,
15};
16use crate::crd::{Bind9Cluster, Bind9Instance, ConfigMapRefs, ImageConfig};
17use crate::labels::{
18 APP_NAME_BIND9, COMPONENT_DNS_CLUSTER, COMPONENT_DNS_SERVER, K8S_COMPONENT, K8S_INSTANCE,
19 K8S_MANAGED_BY, K8S_NAME, K8S_PART_OF, MANAGED_BY_BIND9_CLUSTER, MANAGED_BY_BIND9_INSTANCE,
20 PART_OF_BINDY,
21};
22use k8s_openapi::api::{
23 apps::v1::{Deployment, DeploymentSpec},
24 core::v1::{
25 Capabilities, ConfigMap, Container, ContainerPort, EnvVar, EnvVarSource,
26 PodSecurityContext, PodSpec, PodTemplateSpec, Probe, SecretKeySelector, SecurityContext,
27 Service, ServiceAccount, ServicePort, ServiceSpec, TCPSocketAction, Volume, VolumeMount,
28 },
29};
30use k8s_openapi::apimachinery::pkg::{
31 apis::meta::v1::{LabelSelector, ObjectMeta, OwnerReference},
32 util::intstr::IntOrString,
33};
34use kube::ResourceExt;
35use std::collections::BTreeMap;
36use tracing::{debug, warn};
37
38const NAMED_CONF_TEMPLATE: &str = include_str!("../templates/named.conf.tmpl");
40const NAMED_CONF_OPTIONS_TEMPLATE: &str = include_str!("../templates/named.conf.options.tmpl");
41const RNDC_CONF_TEMPLATE: &str = include_str!("../templates/rndc.conf.tmpl");
42
43const DNSSEC_POLICY_TEMPLATE: &str = r#"
45dnssec-policy "{{POLICY_NAME}}" {
46 // Key configuration
47 keys {
48 ksk lifetime {{KSK_LIFETIME}} algorithm {{ALGORITHM}};
49 zsk lifetime {{ZSK_LIFETIME}} algorithm {{ALGORITHM}};
50 };
51
52 // Authenticated denial of existence
53 {{NSEC_CONFIG}};
54
55 // Signature validity periods
56 signatures-refresh 5d;
57 signatures-validity 30d;
58 signatures-validity-dnskey 30d;
59
60 // Zone propagation delay (time for zone updates to reach all servers)
61 zone-propagation-delay 300; // 5 minutes
62
63 // Parent propagation delay (time for DS updates in parent zone)
64 parent-propagation-delay 3600; // 1 hour
65
66 // Maximum zone TTL (affects key rollover timing)
67 max-zone-ttl 86400; // 24 hours
68};
69"#;
70
71const BIND_ZONES_PATH: &str = "/etc/bind/zones";
73const BIND_CACHE_PATH: &str = "/var/cache/bind";
74const BIND_KEYS_PATH: &str = "/etc/bind/keys";
75const BIND_DNSSEC_KEYS_PATH: &str = "/var/cache/bind/keys";
76const BIND_NAMED_CONF_PATH: &str = "/etc/bind/named.conf";
77const BIND_NAMED_CONF_OPTIONS_PATH: &str = "/etc/bind/named.conf.options";
78const BIND_NAMED_CONF_ZONES_PATH: &str = "/etc/bind/named.conf.zones";
79const BIND_RNDC_CONF_PATH: &str = "/etc/bind/rndc.conf";
80
81const NAMED_CONF_FILENAME: &str = "named.conf";
83const NAMED_CONF_OPTIONS_FILENAME: &str = "named.conf.options";
84const NAMED_CONF_ZONES_FILENAME: &str = "named.conf.zones";
85const RNDC_CONF_FILENAME: &str = "rndc.conf";
86
87const VOLUME_ZONES: &str = "zones";
89const VOLUME_CACHE: &str = "cache";
90const VOLUME_RNDC_KEY: &str = "rndc-key";
91const VOLUME_CONFIG: &str = "config";
92const VOLUME_NAMED_CONF: &str = "named-conf";
93const VOLUME_NAMED_CONF_OPTIONS: &str = "named-conf-options";
94const VOLUME_NAMED_CONF_ZONES: &str = "named-conf-zones";
95const VOLUME_DNSSEC_KEYS: &str = "dnssec-keys";
96
97const DEFAULT_DNSSEC_POLICY_NAME: &str = "default";
99const DEFAULT_DNSSEC_ALGORITHM: &str = "ECDSAP256SHA256";
100const DEFAULT_KSK_LIFETIME: &str = "unlimited";
101const DEFAULT_ZSK_LIFETIME: &str = "unlimited";
102const DEFAULT_NSEC3_SALT_LENGTH: u8 = 16;
103
104pub(crate) fn generate_dnssec_policies(
118 global_config: Option<&crate::crd::Bind9Config>,
119 instance_config: Option<&crate::crd::Bind9Config>,
120) -> String {
121 let dnssec_config = if let Some(instance) = instance_config {
123 instance.dnssec.as_ref().and_then(|d| d.signing.as_ref())
124 } else {
125 global_config.and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
126 };
127
128 let Some(signing) = dnssec_config else {
130 return String::new();
131 };
132
133 if !signing.enabled {
134 return String::new();
135 }
136
137 let policy_name = signing
139 .policy
140 .as_deref()
141 .unwrap_or(DEFAULT_DNSSEC_POLICY_NAME);
142 let algorithm = signing
143 .algorithm
144 .as_deref()
145 .unwrap_or(DEFAULT_DNSSEC_ALGORITHM);
146 let ksk_lifetime = signing
147 .ksk_lifetime
148 .as_deref()
149 .unwrap_or(DEFAULT_KSK_LIFETIME);
150 let zsk_lifetime = signing
151 .zsk_lifetime
152 .as_deref()
153 .unwrap_or(DEFAULT_ZSK_LIFETIME);
154
155 let nsec_config = if signing.nsec3.unwrap_or(false) {
157 let iterations = signing.nsec3_iterations.unwrap_or(0);
158 let salt_length = DEFAULT_NSEC3_SALT_LENGTH;
159 format!("nsec3param iterations {iterations} optout no salt-length {salt_length}")
160 } else {
161 "nsec".to_string()
162 };
163
164 DNSSEC_POLICY_TEMPLATE
166 .replace("{{POLICY_NAME}}", policy_name)
167 .replace("{{ALGORITHM}}", algorithm)
168 .replace("{{KSK_LIFETIME}}", ksk_lifetime)
169 .replace("{{ZSK_LIFETIME}}", zsk_lifetime)
170 .replace("{{NSEC_CONFIG}}", &nsec_config)
171}
172
173#[allow(dead_code)]
186pub(crate) fn is_dnssec_signing_enabled(
187 global_config: Option<&crate::crd::Bind9Config>,
188 instance_config: Option<&crate::crd::Bind9Config>,
189) -> bool {
190 let dnssec_config = if let Some(instance) = instance_config {
192 instance.dnssec.as_ref().and_then(|d| d.signing.as_ref())
193 } else {
194 global_config.and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
195 };
196
197 dnssec_config.is_some_and(|signing| signing.enabled)
198}
199
200pub(crate) fn get_dnssec_signing_config<'a>(
213 global_config: Option<&'a crate::crd::Bind9Config>,
214 instance_config: Option<&'a crate::crd::Bind9Config>,
215) -> Option<&'a crate::crd::DNSSECSigningConfig> {
216 if let Some(instance) = instance_config {
218 if let Some(config) = instance.dnssec.as_ref().and_then(|d| d.signing.as_ref()) {
219 if config.enabled {
220 return Some(config);
221 }
222 }
223 }
224
225 global_config
226 .and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
227 .filter(|config| config.enabled)
228}
229
230pub(crate) fn build_dnssec_key_volumes(
246 global_config: Option<&crate::crd::Bind9Config>,
247 instance_config: Option<&crate::crd::Bind9Config>,
248) -> (Vec<Volume>, Vec<VolumeMount>) {
249 use k8s_openapi::api::core::v1::{
250 EmptyDirVolumeSource, SecretVolumeSource, Volume, VolumeMount,
251 };
252
253 let Some(signing_config) = get_dnssec_signing_config(global_config, instance_config) else {
254 return (vec![], vec![]);
255 };
256
257 let mut volumes = Vec::new();
258 let mut volume_mounts = Vec::new();
259
260 match &signing_config.keys_from {
262 Some(crate::crd::DNSSECKeySource {
264 secret_ref: Some(secret),
265 ..
266 }) => {
267 volumes.push(Volume {
268 name: VOLUME_DNSSEC_KEYS.to_string(),
269 secret: Some(SecretVolumeSource {
270 secret_name: Some(secret.name.clone()),
271 default_mode: Some(0o600), ..Default::default()
273 }),
274 ..Default::default()
275 });
276
277 volume_mounts.push(VolumeMount {
278 name: VOLUME_DNSSEC_KEYS.to_string(),
279 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
280 read_only: Some(false), ..Default::default()
282 });
283
284 debug!(
285 secret_name = %secret.name,
286 "Mounting user-supplied DNSSEC keys from Secret"
287 );
288 }
289
290 None
293 | Some(crate::crd::DNSSECKeySource {
294 secret_ref: None,
295 persistent_volume: None,
296 }) => {
297 if signing_config.auto_generate.unwrap_or(true) {
298 volumes.push(Volume {
299 name: VOLUME_DNSSEC_KEYS.to_string(),
300 empty_dir: Some(EmptyDirVolumeSource::default()),
301 ..Default::default()
302 });
303
304 volume_mounts.push(VolumeMount {
305 name: VOLUME_DNSSEC_KEYS.to_string(),
306 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
307 ..Default::default()
308 });
309
310 debug!("DNSSEC keys will be auto-generated by BIND9 in emptyDir");
311
312 if signing_config.export_to_secret.unwrap_or(true) {
313 debug!("Auto-generated keys will be exported to Secret for backup/restore");
314 }
315 }
316 }
317
318 Some(crate::crd::DNSSECKeySource {
320 persistent_volume: Some(_pvc),
321 ..
322 }) => {
323 warn!("Persistent storage for DNSSEC keys is not yet implemented - using emptyDir");
324 volumes.push(Volume {
325 name: VOLUME_DNSSEC_KEYS.to_string(),
326 empty_dir: Some(EmptyDirVolumeSource::default()),
327 ..Default::default()
328 });
329
330 volume_mounts.push(VolumeMount {
331 name: VOLUME_DNSSEC_KEYS.to_string(),
332 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
333 ..Default::default()
334 });
335 }
336 }
337
338 (volumes, volume_mounts)
339}
340
341#[must_use]
367pub fn build_cluster_labels(cluster_name: &str) -> BTreeMap<String, String> {
368 let mut labels = BTreeMap::new();
369 labels.insert("app".into(), APP_NAME_BIND9.into());
370 labels.insert("cluster".into(), cluster_name.into());
371 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
372 labels.insert(K8S_INSTANCE.into(), cluster_name.into());
373 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_CLUSTER.into());
374 labels.insert(K8S_MANAGED_BY.into(), MANAGED_BY_BIND9_CLUSTER.into());
375 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
376 labels
377}
378
379#[must_use]
400pub fn build_labels_from_instance(
401 instance_name: &str,
402 instance: &Bind9Instance,
403) -> BTreeMap<String, String> {
404 use crate::labels::{BINDY_MANAGED_BY_LABEL, BINDY_ROLE_LABEL};
405
406 let mut labels = BTreeMap::new();
407 labels.insert("app".into(), APP_NAME_BIND9.into());
408 labels.insert("instance".into(), instance_name.into());
409 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
410 labels.insert(K8S_INSTANCE.into(), instance_name.into());
411 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
412 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
413
414 let managed_by = instance
417 .metadata
418 .labels
419 .as_ref()
420 .and_then(|labels| labels.get(BINDY_MANAGED_BY_LABEL))
421 .map_or(MANAGED_BY_BIND9_INSTANCE, String::as_str);
422
423 labels.insert(K8S_MANAGED_BY.into(), managed_by.into());
424
425 if let Some(instance_labels) = &instance.metadata.labels {
428 if let Some(role) = instance_labels.get(BINDY_ROLE_LABEL) {
429 labels.insert(BINDY_ROLE_LABEL.into(), role.clone());
430 }
431 }
432
433 labels
434}
435
436#[must_use]
449pub fn build_owner_references(instance: &Bind9Instance) -> Vec<OwnerReference> {
450 vec![OwnerReference {
451 api_version: API_GROUP_VERSION.to_string(),
452 kind: KIND_BIND9_INSTANCE.to_string(),
453 name: instance.name_any(),
454 uid: instance.metadata.uid.clone().unwrap_or_default(),
455 controller: Some(true),
456 block_owner_deletion: Some(true),
457 }]
458}
459
460#[must_use]
480pub fn build_configmap(
481 name: &str,
482 namespace: &str,
483 instance: &Bind9Instance,
484 cluster: Option<&Bind9Cluster>,
485 role_allow_transfer: Option<&Vec<String>>,
486) -> Option<ConfigMap> {
487 debug!(
488 name = %name,
489 namespace = %namespace,
490 "Building ConfigMap for Bind9Instance"
491 );
492
493 let config_map_refs = instance
495 .spec
496 .config_map_refs
497 .as_ref()
498 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
499
500 if let Some(refs) = config_map_refs {
502 if refs.named_conf.is_some() || refs.named_conf_options.is_some() {
503 debug!(
504 named_conf_ref = ?refs.named_conf,
505 named_conf_options_ref = ?refs.named_conf_options,
506 "Custom ConfigMaps specified, skipping generation"
507 );
508 return None;
510 }
511 }
512
513 let mut data = BTreeMap::new();
515 let labels = build_labels_from_instance(name, instance);
516
517 let named_conf = build_named_conf(instance, cluster);
519 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
520
521 let options_conf = build_options_conf(instance, cluster, role_allow_transfer);
523 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
524
525 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
527
528 let owner_refs = build_owner_references(instance);
532
533 Some(ConfigMap {
534 metadata: ObjectMeta {
535 name: Some(format!("{name}-config")),
536 namespace: Some(namespace.into()),
537 labels: Some(labels),
538 owner_references: Some(owner_refs),
539 ..Default::default()
540 },
541 data: Some(data),
542 ..Default::default()
543 })
544}
545
546pub fn build_cluster_configmap(
566 cluster_name: &str,
567 namespace: &str,
568 cluster: &Bind9Cluster,
569) -> Result<ConfigMap, anyhow::Error> {
570 debug!(
571 cluster_name = %cluster_name,
572 namespace = %namespace,
573 "Building cluster-level shared ConfigMap"
574 );
575
576 let mut data = BTreeMap::new();
578 let labels = build_cluster_labels(cluster_name);
579
580 let named_conf = build_cluster_named_conf(cluster);
582 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
583
584 let options_conf = build_cluster_options_conf(cluster);
586 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
587
588 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
590
591 Ok(ConfigMap {
592 metadata: ObjectMeta {
593 name: Some(format!("{cluster_name}-config")),
594 namespace: Some(namespace.into()),
595 labels: Some(labels),
596 ..Default::default()
597 },
598 data: Some(data),
599 ..Default::default()
600 })
601}
602
603fn build_named_conf(instance: &Bind9Instance, cluster: Option<&Bind9Cluster>) -> String {
617 let config_map_refs = instance
619 .spec
620 .config_map_refs
621 .as_ref()
622 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
623
624 let zones_include = if let Some(refs) = config_map_refs {
625 if refs.named_conf_zones.is_some() {
626 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
628 } else {
629 String::new()
631 }
632 } else {
633 String::new()
635 };
636
637 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
641 let rndc_key_names = "\"bindy-operator\"";
642
643 NAMED_CONF_TEMPLATE
644 .replace("{{ZONES_INCLUDE}}", &zones_include)
645 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
646 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
647}
648
649#[allow(clippy::too_many_lines)]
670fn build_options_conf(
671 instance: &Bind9Instance,
672 cluster: Option<&Bind9Cluster>,
673 role_allow_transfer: Option<&Vec<String>>,
674) -> String {
675 let recursion;
676 let mut allow_query = String::new();
677 let allow_transfer;
678 let mut dnssec_validate = String::new();
679
680 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
682
683 if let Some(config) = &instance.spec.config {
684 let recursion_value = if let Some(rec) = config.recursion {
686 if rec {
687 "yes"
688 } else {
689 "no"
690 }
691 } else if let Some(global) = global_config {
692 if global.recursion.unwrap_or(false) {
693 "yes"
694 } else {
695 "no"
696 }
697 } else {
698 "no"
699 };
700 recursion = format!("recursion {recursion_value};");
701
702 if let Some(acls) = &config.allow_query {
704 if !acls.is_empty() {
705 let acl_list = acls.join("; ");
706 allow_query = format!("allow-query {{ {acl_list}; }};");
707 }
708 } else if let Some(global) = global_config {
709 if let Some(global_acls) = &global.allow_query {
710 if !global_acls.is_empty() {
711 let acl_list = global_acls.join("; ");
712 allow_query = format!("allow-query {{ {acl_list}; }};");
713 }
714 }
715 }
716
717 if let Some(acls) = &config.allow_transfer {
719 let acl_list = if acls.is_empty() {
721 "none".to_string()
722 } else {
723 acls.join("; ")
724 };
725 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
726 } else if let Some(role_acls) = role_allow_transfer {
727 let acl_list = if role_acls.is_empty() {
729 "none".to_string()
730 } else {
731 role_acls.join("; ")
732 };
733 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
734 } else if let Some(global) = global_config {
735 if let Some(global_acls) = &global.allow_transfer {
737 let acl_list = if global_acls.is_empty() {
738 "none".to_string()
739 } else {
740 global_acls.join("; ")
741 };
742 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
743 } else {
744 allow_transfer = String::new();
745 }
746 } else {
747 allow_transfer = String::new();
749 }
750
751 if let Some(dnssec) = &config.dnssec {
755 if dnssec.validation.unwrap_or(false) {
756 dnssec_validate = "dnssec-validation yes;".to_string();
757 } else {
758 dnssec_validate = "dnssec-validation no;".to_string();
759 }
760 } else if let Some(global) = global_config {
761 if let Some(global_dnssec) = &global.dnssec {
762 if global_dnssec.validation.unwrap_or(false) {
763 dnssec_validate = "dnssec-validation yes;".to_string();
764 } else {
765 dnssec_validate = "dnssec-validation no;".to_string();
766 }
767 }
768 }
769 } else {
770 if let Some(global) = global_config {
772 let recursion_value = if global.recursion.unwrap_or(false) {
774 "yes"
775 } else {
776 "no"
777 };
778 recursion = format!("recursion {recursion_value};");
779
780 if let Some(acls) = &global.allow_query {
782 if !acls.is_empty() {
783 let acl_list = acls.join("; ");
784 allow_query = format!("allow-query {{ {acl_list}; }};");
785 }
786 }
787
788 if let Some(role_acls) = role_allow_transfer {
790 let acl_list = if role_acls.is_empty() {
791 "none".to_string()
792 } else {
793 role_acls.join("; ")
794 };
795 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
796 } else if let Some(global_acls) = &global.allow_transfer {
797 let acl_list = if global_acls.is_empty() {
798 "none".to_string()
799 } else {
800 global_acls.join("; ")
801 };
802 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
803 } else {
804 allow_transfer = String::new();
805 }
806
807 if let Some(dnssec) = &global.dnssec {
809 if dnssec.validation.unwrap_or(false) {
810 dnssec_validate = "dnssec-validation yes;".to_string();
811 }
812 }
813 } else {
814 recursion = "recursion no;".to_string();
816 allow_transfer = String::new();
818 }
819 }
820
821 let dnssec_policies = generate_dnssec_policies(global_config, instance.spec.config.as_ref());
823
824 NAMED_CONF_OPTIONS_TEMPLATE
826 .replace("{{RECURSION}}", &recursion)
827 .replace("{{ALLOW_QUERY}}", &allow_query)
828 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
829 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
830 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies)
831}
832
833fn build_cluster_named_conf(cluster: &Bind9Cluster) -> String {
846 let zones_include = if let Some(refs) = &cluster.spec.common.config_map_refs {
848 if refs.named_conf_zones.is_some() {
849 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
851 } else {
852 String::new()
854 }
855 } else {
856 String::new()
858 };
859
860 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
864 let rndc_key_names = "\"bindy-operator\"";
865
866 NAMED_CONF_TEMPLATE
867 .replace("{{ZONES_INCLUDE}}", &zones_include)
868 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
869 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
870}
871
872#[allow(clippy::too_many_lines)]
885fn build_cluster_options_conf(cluster: &Bind9Cluster) -> String {
886 let recursion;
887 let mut allow_query = String::new();
888 let mut allow_transfer = String::new();
889 let mut dnssec_validate = String::new();
890
891 if let Some(global) = &cluster.spec.common.global {
893 let recursion_value = if global.recursion.unwrap_or(false) {
895 "yes"
896 } else {
897 "no"
898 };
899 recursion = format!("recursion {recursion_value};");
900
901 if let Some(aq) = &global.allow_query {
903 if !aq.is_empty() {
904 allow_query = format!(
905 "allow-query {{ {}; }};",
906 aq.iter().map(String::as_str).collect::<Vec<_>>().join("; ")
907 );
908 }
909 }
910
911 if let Some(at) = &global.allow_transfer {
913 if !at.is_empty() {
914 allow_transfer = format!(
915 "allow-transfer {{ {}; }};",
916 at.iter().map(String::as_str).collect::<Vec<_>>().join("; ")
917 );
918 }
919 }
920
921 if let Some(dnssec) = &global.dnssec {
923 if dnssec.validation.unwrap_or(false) {
924 dnssec_validate = "dnssec-validation yes;".to_string();
925 } else {
926 dnssec_validate = "dnssec-validation no;".to_string();
927 }
928 }
929 } else {
930 recursion = "recursion no;".to_string();
932 }
933
934 let dnssec_policies = generate_dnssec_policies(cluster.spec.common.global.as_ref(), None);
936
937 NAMED_CONF_OPTIONS_TEMPLATE
938 .replace("{{RECURSION}}", &recursion)
939 .replace("{{ALLOW_QUERY}}", &allow_query)
940 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
941 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
942 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies)
943}
944
945#[must_use]
965struct DeploymentConfig<'a> {
967 image_config: Option<&'a ImageConfig>,
968 config_map_refs: Option<&'a ConfigMapRefs>,
969 version: &'a str,
970 volumes: Option<&'a Vec<Volume>>,
971 volume_mounts: Option<&'a Vec<VolumeMount>>,
972 bindcar_config: Option<&'a crate::crd::BindcarConfig>,
973 configmap_name: String,
974 rndc_secret_name: String,
975}
976
977fn resolve_deployment_config<'a>(
979 name: &str,
980 instance: &'a Bind9Instance,
981 cluster: Option<&'a Bind9Cluster>,
982 cluster_provider: Option<&'a crate::crd::ClusterBind9Provider>,
983) -> DeploymentConfig<'a> {
984 let image_config = instance
986 .spec
987 .image
988 .as_ref()
989 .or_else(|| cluster.and_then(|c| c.spec.common.image.as_ref()))
990 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.image.as_ref()));
991
992 let config_map_refs = instance
994 .spec
995 .config_map_refs
996 .as_ref()
997 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()))
998 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.config_map_refs.as_ref()));
999
1000 let version = instance
1002 .spec
1003 .version
1004 .as_deref()
1005 .or_else(|| cluster.and_then(|c| c.spec.common.version.as_deref()))
1006 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.version.as_deref()))
1007 .unwrap_or(DEFAULT_BIND9_VERSION);
1008
1009 let volumes = instance
1011 .spec
1012 .volumes
1013 .as_ref()
1014 .or_else(|| cluster.and_then(|c| c.spec.common.volumes.as_ref()))
1015 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volumes.as_ref()));
1016
1017 let volume_mounts = instance
1019 .spec
1020 .volume_mounts
1021 .as_ref()
1022 .or_else(|| cluster.and_then(|c| c.spec.common.volume_mounts.as_ref()))
1023 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volume_mounts.as_ref()));
1024
1025 let bindcar_config = instance
1027 .spec
1028 .bindcar_config
1029 .as_ref()
1030 .or_else(|| {
1031 cluster.and_then(|c| {
1032 c.spec
1033 .common
1034 .global
1035 .as_ref()
1036 .and_then(|g| g.bindcar_config.as_ref())
1037 })
1038 })
1039 .or_else(|| {
1040 cluster_provider.and_then(|cp| {
1041 cp.spec
1042 .common
1043 .global
1044 .as_ref()
1045 .and_then(|g| g.bindcar_config.as_ref())
1046 })
1047 });
1048
1049 let configmap_name = if instance.spec.cluster_ref.is_empty() {
1051 format!("{name}-config")
1053 } else {
1054 format!("{}-config", instance.spec.cluster_ref)
1056 };
1057
1058 let rndc_secret_name = format!("{name}-rndc-key");
1061
1062 DeploymentConfig {
1063 image_config,
1064 config_map_refs,
1065 version,
1066 volumes,
1067 volume_mounts,
1068 bindcar_config,
1069 configmap_name,
1070 rndc_secret_name,
1071 }
1072}
1073
1074pub fn build_deployment(
1075 name: &str,
1076 namespace: &str,
1077 instance: &Bind9Instance,
1078 cluster: Option<&Bind9Cluster>,
1079 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1080) -> Deployment {
1081 debug!(
1082 name = %name,
1083 namespace = %namespace,
1084 has_cluster = cluster.is_some(),
1085 has_cluster_provider = cluster_provider.is_some(),
1086 "Building Deployment for Bind9Instance"
1087 );
1088
1089 let labels = build_labels_from_instance(name, instance);
1091 let replicas = instance.spec.replicas.unwrap_or(1);
1092 debug!(replicas, "Deployment replica count");
1093
1094 let config = resolve_deployment_config(name, instance, cluster, cluster_provider);
1095
1096 let owner_refs = build_owner_references(instance);
1097
1098 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
1100 let instance_config = instance.spec.config.as_ref();
1101
1102 let (dnssec_volumes, dnssec_volume_mounts) =
1104 build_dnssec_key_volumes(global_config, instance_config);
1105
1106 let all_volumes = if dnssec_volumes.is_empty() {
1108 config.volumes.map(std::borrow::ToOwned::to_owned)
1109 } else {
1110 let mut merged = dnssec_volumes;
1111 if let Some(custom) = config.volumes {
1112 merged.extend(custom.iter().cloned());
1113 }
1114 Some(merged)
1115 };
1116
1117 let all_volume_mounts = if dnssec_volume_mounts.is_empty() {
1119 config.volume_mounts.map(std::borrow::ToOwned::to_owned)
1120 } else {
1121 let mut merged = dnssec_volume_mounts;
1122 if let Some(custom) = config.volume_mounts {
1123 merged.extend(custom.iter().cloned());
1124 }
1125 Some(merged)
1126 };
1127
1128 Deployment {
1129 metadata: ObjectMeta {
1130 name: Some(name.into()),
1131 namespace: Some(namespace.into()),
1132 labels: Some(labels.clone()),
1133 owner_references: Some(owner_refs),
1134 ..Default::default()
1135 },
1136 spec: Some(DeploymentSpec {
1137 replicas: Some(replicas),
1138 selector: LabelSelector {
1139 match_labels: Some(labels.clone()),
1140 ..Default::default()
1141 },
1142 template: PodTemplateSpec {
1143 metadata: Some(ObjectMeta {
1144 labels: Some(labels.clone()),
1145 ..Default::default()
1146 }),
1147 spec: Some(build_pod_spec(
1148 namespace,
1149 &config.configmap_name,
1150 &config.rndc_secret_name,
1151 config.version,
1152 config.image_config,
1153 config.config_map_refs,
1154 all_volumes.as_ref(),
1155 all_volume_mounts.as_ref(),
1156 config.bindcar_config,
1157 )),
1158 },
1159 ..Default::default()
1160 }),
1161 ..Default::default()
1162 }
1163}
1164
1165#[allow(clippy::too_many_arguments)]
1178#[allow(clippy::too_many_lines)]
1179fn build_pod_spec(
1180 namespace: &str,
1181 configmap_name: &str,
1182 rndc_secret_name: &str,
1183 version: &str,
1184 image_config: Option<&ImageConfig>,
1185 config_map_refs: Option<&ConfigMapRefs>,
1186 custom_volumes: Option<&Vec<Volume>>,
1187 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1188 bindcar_config: Option<&crate::crd::BindcarConfig>,
1189) -> PodSpec {
1190 let image = if let Some(img_cfg) = image_config {
1192 img_cfg
1193 .image
1194 .clone()
1195 .unwrap_or_else(|| format!("internetsystemsconsortium/bind9:{version}"))
1196 } else {
1197 format!("internetsystemsconsortium/bind9:{version}")
1198 };
1199
1200 let image_pull_policy = image_config
1202 .and_then(|cfg| cfg.image_pull_policy.clone())
1203 .unwrap_or_else(|| "IfNotPresent".into());
1204
1205 let bind9_container = Container {
1207 name: CONTAINER_NAME_BIND9.into(),
1208 image: Some(image),
1209 image_pull_policy: Some(image_pull_policy),
1210 command: Some(vec!["named".into()]),
1211 args: Some(vec![
1212 "-c".into(),
1213 BIND_NAMED_CONF_PATH.into(),
1214 "-g".into(), ]),
1216 ports: Some(vec![
1217 ContainerPort {
1218 name: Some("dns-tcp".into()),
1219 container_port: i32::from(DNS_CONTAINER_PORT),
1220 protocol: Some("TCP".into()),
1221 ..Default::default()
1222 },
1223 ContainerPort {
1224 name: Some("dns-udp".into()),
1225 container_port: i32::from(DNS_CONTAINER_PORT),
1226 protocol: Some("UDP".into()),
1227 ..Default::default()
1228 },
1229 ContainerPort {
1230 name: Some("rndc".into()),
1231 container_port: i32::from(RNDC_PORT),
1232 protocol: Some("TCP".into()),
1233 ..Default::default()
1234 },
1235 ]),
1236 env: Some(vec![
1237 EnvVar {
1238 name: "TZ".into(),
1239 value: Some("UTC".into()),
1240 ..Default::default()
1241 },
1242 EnvVar {
1243 name: "MALLOC_CONF".into(),
1244 value: Some(BIND9_MALLOC_CONF.into()),
1245 ..Default::default()
1246 },
1247 ]),
1248 volume_mounts: Some(build_volume_mounts(config_map_refs, custom_volume_mounts)),
1249 liveness_probe: Some(Probe {
1250 tcp_socket: Some(TCPSocketAction {
1251 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1252 ..Default::default()
1253 }),
1254 initial_delay_seconds: Some(LIVENESS_INITIAL_DELAY_SECS),
1255 period_seconds: Some(LIVENESS_PERIOD_SECS),
1256 timeout_seconds: Some(LIVENESS_TIMEOUT_SECS),
1257 failure_threshold: Some(LIVENESS_FAILURE_THRESHOLD),
1258 ..Default::default()
1259 }),
1260 readiness_probe: Some(Probe {
1261 tcp_socket: Some(TCPSocketAction {
1262 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1263 ..Default::default()
1264 }),
1265 initial_delay_seconds: Some(READINESS_INITIAL_DELAY_SECS),
1266 period_seconds: Some(READINESS_PERIOD_SECS),
1267 timeout_seconds: Some(READINESS_TIMEOUT_SECS),
1268 failure_threshold: Some(READINESS_FAILURE_THRESHOLD),
1269 ..Default::default()
1270 }),
1271 security_context: Some(SecurityContext {
1272 run_as_non_root: Some(true),
1273 run_as_user: Some(BIND9_NONROOT_UID),
1274 run_as_group: Some(BIND9_NONROOT_UID),
1275 allow_privilege_escalation: Some(false),
1276 capabilities: Some(Capabilities {
1277 drop: Some(vec!["ALL".to_string()]),
1278 ..Default::default()
1279 }),
1280 ..Default::default()
1281 }),
1282 ..Default::default()
1283 };
1284
1285 let image_pull_secrets = image_config.and_then(|cfg| {
1287 cfg.image_pull_secrets.as_ref().map(|secrets| {
1288 secrets
1289 .iter()
1290 .map(|s| k8s_openapi::api::core::v1::LocalObjectReference { name: s.clone() })
1291 .collect()
1292 })
1293 });
1294
1295 PodSpec {
1296 containers: {
1297 let mut containers = vec![bind9_container];
1298 containers.push(build_api_sidecar_container(
1299 namespace,
1300 bindcar_config,
1301 rndc_secret_name,
1302 ));
1303 containers
1304 },
1305 volumes: Some(build_volumes(
1306 configmap_name,
1307 rndc_secret_name,
1308 config_map_refs,
1309 custom_volumes,
1310 )),
1311 image_pull_secrets,
1312 service_account_name: Some(BIND9_SERVICE_ACCOUNT.into()),
1313 security_context: Some(PodSecurityContext {
1314 run_as_user: Some(BIND9_NONROOT_UID),
1315 run_as_group: Some(BIND9_NONROOT_UID),
1316 fs_group: Some(BIND9_NONROOT_UID),
1317 run_as_non_root: Some(true),
1318 ..Default::default()
1319 }),
1320 ..Default::default()
1321 }
1322}
1323
1324#[allow(clippy::too_many_lines)]
1336fn build_api_sidecar_container(
1337 namespace: &str,
1338 bindcar_config: Option<&crate::crd::BindcarConfig>,
1339 rndc_secret_name: &str,
1340) -> Container {
1341 let image = bindcar_config
1343 .and_then(|c| c.image.clone())
1344 .unwrap_or_else(|| crate::constants::DEFAULT_BINDCAR_IMAGE.to_string());
1345
1346 let image_pull_policy = bindcar_config
1347 .and_then(|c| c.image_pull_policy.clone())
1348 .unwrap_or_else(|| "IfNotPresent".to_string());
1349
1350 let port = bindcar_config
1351 .and_then(|c| c.port)
1352 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
1353
1354 let log_level = bindcar_config
1355 .and_then(|c| c.log_level.clone())
1356 .unwrap_or_else(|| "info".to_string());
1357
1358 let resources = bindcar_config.and_then(|c| c.resources.clone());
1359
1360 let mut env_vars = vec![
1362 EnvVar {
1363 name: "BIND_ZONE_DIR".into(),
1364 value: Some(BIND_CACHE_PATH.into()),
1365 ..Default::default()
1366 },
1367 EnvVar {
1368 name: "API_PORT".into(),
1369 value: Some(port.to_string()),
1370 ..Default::default()
1371 },
1372 EnvVar {
1373 name: "RUST_LOG".into(),
1374 value: Some(log_level),
1375 ..Default::default()
1376 },
1377 EnvVar {
1378 name: "BIND_ALLOWED_SERVICE_ACCOUNTS".into(),
1379 value: Some(format!(
1380 "system:serviceaccount:{namespace}:{BIND9_SERVICE_ACCOUNT}"
1381 )),
1382 ..Default::default()
1383 },
1384 EnvVar {
1385 name: "RNDC_SECRET".into(),
1386 value_from: Some(EnvVarSource {
1387 secret_key_ref: Some(SecretKeySelector {
1388 name: rndc_secret_name.to_string(),
1389 key: "secret".to_string(),
1390 optional: Some(false),
1391 }),
1392 ..Default::default()
1393 }),
1394 ..Default::default()
1395 },
1396 EnvVar {
1397 name: "RNDC_ALGORITHM".into(),
1398 value_from: Some(EnvVarSource {
1399 secret_key_ref: Some(SecretKeySelector {
1400 name: rndc_secret_name.to_string(),
1401 key: "algorithm".to_string(),
1402 optional: Some(false),
1403 }),
1404 ..Default::default()
1405 }),
1406 ..Default::default()
1407 },
1408 ];
1409
1410 if let Some(config) = bindcar_config {
1412 if let Some(user_env_vars) = &config.env_vars {
1413 env_vars.extend(user_env_vars.clone());
1414 }
1415 }
1416
1417 Container {
1418 name: CONTAINER_NAME_BINDCAR.into(),
1419 image: Some(image),
1420 image_pull_policy: Some(image_pull_policy),
1421 ports: Some(vec![ContainerPort {
1422 name: Some("http".into()),
1423 container_port: port,
1424 protocol: Some("TCP".into()),
1425 ..Default::default()
1426 }]),
1427 env: Some(env_vars),
1428 volume_mounts: Some(vec![
1429 VolumeMount {
1430 name: "cache".into(),
1431 mount_path: BIND_CACHE_PATH.into(),
1432 ..Default::default()
1433 },
1434 VolumeMount {
1435 name: "rndc-key".into(),
1436 mount_path: BIND_KEYS_PATH.into(),
1437 read_only: Some(true),
1438 ..Default::default()
1439 },
1440 VolumeMount {
1441 name: VOLUME_CONFIG.into(),
1442 mount_path: BIND_RNDC_CONF_PATH.into(),
1443 sub_path: Some(RNDC_CONF_FILENAME.into()),
1444 ..Default::default()
1445 },
1446 ]),
1447 resources,
1448 security_context: Some(SecurityContext {
1449 run_as_non_root: Some(true),
1450 run_as_user: Some(BIND9_NONROOT_UID),
1451 run_as_group: Some(BIND9_NONROOT_UID),
1452 allow_privilege_escalation: Some(false),
1453 capabilities: Some(Capabilities {
1454 drop: Some(vec!["ALL".to_string()]),
1455 ..Default::default()
1456 }),
1457 ..Default::default()
1458 }),
1459 ..Default::default()
1460 }
1461}
1462
1463fn build_volume_mounts(
1481 config_map_refs: Option<&ConfigMapRefs>,
1482 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1483) -> Vec<VolumeMount> {
1484 let mut mounts = vec![
1485 VolumeMount {
1486 name: VOLUME_ZONES.into(),
1487 mount_path: BIND_ZONES_PATH.into(),
1488 ..Default::default()
1489 },
1490 VolumeMount {
1491 name: VOLUME_CACHE.into(),
1492 mount_path: BIND_CACHE_PATH.into(),
1493 ..Default::default()
1494 },
1495 VolumeMount {
1496 name: VOLUME_RNDC_KEY.into(),
1497 mount_path: BIND_KEYS_PATH.into(),
1498 read_only: Some(true),
1499 ..Default::default()
1500 },
1501 ];
1502
1503 if let Some(refs) = config_map_refs {
1505 if let Some(_configmap_name) = &refs.named_conf {
1506 mounts.push(VolumeMount {
1507 name: VOLUME_NAMED_CONF.into(),
1508 mount_path: BIND_NAMED_CONF_PATH.into(),
1509 sub_path: Some(NAMED_CONF_FILENAME.into()),
1510 ..Default::default()
1511 });
1512 } else {
1513 mounts.push(VolumeMount {
1515 name: VOLUME_CONFIG.into(),
1516 mount_path: BIND_NAMED_CONF_PATH.into(),
1517 sub_path: Some(NAMED_CONF_FILENAME.into()),
1518 ..Default::default()
1519 });
1520 }
1521
1522 if let Some(_configmap_name) = &refs.named_conf_options {
1523 mounts.push(VolumeMount {
1524 name: VOLUME_NAMED_CONF_OPTIONS.into(),
1525 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1526 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1527 ..Default::default()
1528 });
1529 } else {
1530 mounts.push(VolumeMount {
1532 name: VOLUME_CONFIG.into(),
1533 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1534 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1535 ..Default::default()
1536 });
1537 }
1538
1539 if let Some(_configmap_name) = &refs.named_conf_zones {
1541 mounts.push(VolumeMount {
1542 name: VOLUME_NAMED_CONF_ZONES.into(),
1543 mount_path: BIND_NAMED_CONF_ZONES_PATH.into(),
1544 sub_path: Some(NAMED_CONF_ZONES_FILENAME.into()),
1545 ..Default::default()
1546 });
1547 }
1548 } else {
1550 mounts.push(VolumeMount {
1552 name: VOLUME_CONFIG.into(),
1553 mount_path: BIND_NAMED_CONF_PATH.into(),
1554 sub_path: Some(NAMED_CONF_FILENAME.into()),
1555 ..Default::default()
1556 });
1557 mounts.push(VolumeMount {
1558 name: VOLUME_CONFIG.into(),
1559 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1560 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1561 ..Default::default()
1562 });
1563 }
1565
1566 mounts.push(VolumeMount {
1568 name: VOLUME_CONFIG.into(),
1569 mount_path: BIND_RNDC_CONF_PATH.into(),
1570 sub_path: Some(RNDC_CONF_FILENAME.into()),
1571 ..Default::default()
1572 });
1573
1574 if let Some(custom_mounts) = custom_volume_mounts {
1576 mounts.extend(custom_mounts.iter().cloned());
1577 }
1578
1579 mounts
1580}
1581
1582fn build_volumes(
1603 configmap_name: &str,
1604 rndc_secret_name: &str,
1605 config_map_refs: Option<&ConfigMapRefs>,
1606 custom_volumes: Option<&Vec<Volume>>,
1607) -> Vec<Volume> {
1608 let mut volumes = vec![
1609 Volume {
1610 name: VOLUME_ZONES.into(),
1611 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
1612 ..Default::default()
1613 },
1614 Volume {
1615 name: VOLUME_CACHE.into(),
1616 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
1617 ..Default::default()
1618 },
1619 Volume {
1620 name: VOLUME_RNDC_KEY.into(),
1621 secret: Some(k8s_openapi::api::core::v1::SecretVolumeSource {
1622 secret_name: Some(rndc_secret_name.to_string()),
1623 ..Default::default()
1624 }),
1625 ..Default::default()
1626 },
1627 ];
1628
1629 if let Some(refs) = config_map_refs {
1631 if let Some(configmap_name) = &refs.named_conf {
1632 volumes.push(Volume {
1633 name: VOLUME_NAMED_CONF.into(),
1634 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1635 name: configmap_name.clone(),
1636 ..Default::default()
1637 }),
1638 ..Default::default()
1639 });
1640 }
1641
1642 if let Some(configmap_name) = &refs.named_conf_options {
1643 volumes.push(Volume {
1644 name: VOLUME_NAMED_CONF_OPTIONS.into(),
1645 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1646 name: configmap_name.clone(),
1647 ..Default::default()
1648 }),
1649 ..Default::default()
1650 });
1651 }
1652
1653 if let Some(configmap_name) = &refs.named_conf_zones {
1654 volumes.push(Volume {
1655 name: VOLUME_NAMED_CONF_ZONES.into(),
1656 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1657 name: configmap_name.clone(),
1658 ..Default::default()
1659 }),
1660 ..Default::default()
1661 });
1662 }
1663
1664 if refs.named_conf.is_none() || refs.named_conf_options.is_none() {
1667 volumes.push(Volume {
1668 name: VOLUME_CONFIG.into(),
1669 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1670 name: configmap_name.to_string(),
1671 ..Default::default()
1672 }),
1673 ..Default::default()
1674 });
1675 }
1676 } else {
1677 volumes.push(Volume {
1679 name: VOLUME_CONFIG.into(),
1680 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1681 name: configmap_name.to_string(),
1682 ..Default::default()
1683 }),
1684 ..Default::default()
1685 });
1686 }
1687
1688 if let Some(custom_vols) = custom_volumes {
1690 volumes.extend(custom_vols.iter().cloned());
1691 }
1692
1693 volumes
1694}
1695
1696#[must_use]
1738pub fn build_service(
1739 name: &str,
1740 namespace: &str,
1741 instance: &Bind9Instance,
1742 custom_config: Option<&crate::crd::ServiceConfig>,
1743) -> Service {
1744 let labels = build_labels_from_instance(name, instance);
1746 let owner_refs = build_owner_references(instance);
1747
1748 let api_container_port = instance
1750 .spec
1751 .bindcar_config
1752 .as_ref()
1753 .and_then(|c| c.port)
1754 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
1755
1756 let mut default_spec = ServiceSpec {
1758 selector: Some(labels.clone()),
1759 ports: Some(vec![
1760 ServicePort {
1761 name: Some("dns-tcp".into()),
1762 port: i32::from(DNS_PORT),
1763 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
1764 protocol: Some("TCP".into()),
1765 ..Default::default()
1766 },
1767 ServicePort {
1768 name: Some("dns-udp".into()),
1769 port: i32::from(DNS_PORT),
1770 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
1771 protocol: Some("UDP".into()),
1772 ..Default::default()
1773 },
1774 ServicePort {
1775 name: Some("http".into()),
1776 port: i32::from(crate::constants::BINDCAR_SERVICE_PORT),
1777 target_port: Some(IntOrString::Int(api_container_port)),
1778 protocol: Some("TCP".into()),
1779 ..Default::default()
1780 },
1781 ]),
1782 type_: Some("ClusterIP".into()),
1783 ..Default::default()
1784 };
1785
1786 if let Some(bindcar_service_spec) = instance
1788 .spec
1789 .bindcar_config
1790 .as_ref()
1791 .and_then(|c| c.service_spec.as_ref())
1792 {
1793 merge_service_spec(&mut default_spec, bindcar_service_spec);
1794 }
1795
1796 let (custom_spec, custom_annotations) = custom_config.map_or((None, None), |config| {
1798 (config.spec.as_ref(), config.annotations.as_ref())
1799 });
1800
1801 if let Some(custom) = custom_spec {
1803 merge_service_spec(&mut default_spec, custom);
1804 }
1805
1806 let mut metadata = ObjectMeta {
1808 name: Some(name.into()),
1809 namespace: Some(namespace.into()),
1810 labels: Some(labels),
1811 owner_references: Some(owner_refs),
1812 ..Default::default()
1813 };
1814
1815 if let Some(annotations) = custom_annotations {
1817 metadata.annotations = Some(annotations.clone());
1818 }
1819
1820 Service {
1821 metadata,
1822 spec: Some(default_spec),
1823 ..Default::default()
1824 }
1825}
1826
1827#[must_use]
1854pub fn build_service_account(namespace: &str, _instance: &Bind9Instance) -> ServiceAccount {
1855 let mut labels = BTreeMap::new();
1862 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
1863 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
1864 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
1865
1866 ServiceAccount {
1867 metadata: ObjectMeta {
1868 name: Some(BIND9_SERVICE_ACCOUNT.into()),
1869 namespace: Some(namespace.into()),
1870 labels: Some(labels),
1871 owner_references: None, ..Default::default()
1873 },
1874 ..Default::default()
1875 }
1876}
1877
1878fn merge_service_spec(default: &mut ServiceSpec, custom: &ServiceSpec) {
1886 if let Some(ref type_) = custom.type_ {
1888 default.type_ = Some(type_.clone());
1889 }
1890
1891 if let Some(ref lb_ip) = custom.load_balancer_ip {
1893 default.load_balancer_ip = Some(lb_ip.clone());
1894 }
1895
1896 if let Some(ref affinity) = custom.session_affinity {
1898 default.session_affinity = Some(affinity.clone());
1899 }
1900
1901 if let Some(ref config) = custom.session_affinity_config {
1903 default.session_affinity_config = Some(config.clone());
1904 }
1905
1906 if let Some(ref cluster_ip) = custom.cluster_ip {
1908 default.cluster_ip = Some(cluster_ip.clone());
1909 }
1910
1911 if let Some(ref policy) = custom.external_traffic_policy {
1913 default.external_traffic_policy = Some(policy.clone());
1914 }
1915
1916 if let Some(ref ranges) = custom.load_balancer_source_ranges {
1918 default.load_balancer_source_ranges = Some(ranges.clone());
1919 }
1920
1921 if let Some(ref ips) = custom.external_ips {
1923 default.external_ips = Some(ips.clone());
1924 }
1925
1926 if let Some(ref class) = custom.load_balancer_class {
1928 default.load_balancer_class = Some(class.clone());
1929 }
1930
1931 if let Some(port) = custom.health_check_node_port {
1933 default.health_check_node_port = Some(port);
1934 }
1935
1936 if let Some(publish) = custom.publish_not_ready_addresses {
1938 default.publish_not_ready_addresses = Some(publish);
1939 }
1940
1941 if let Some(allocate) = custom.allocate_load_balancer_node_ports {
1943 default.allocate_load_balancer_node_ports = Some(allocate);
1944 }
1945
1946 if let Some(ref policy) = custom.internal_traffic_policy {
1948 default.internal_traffic_policy = Some(policy.clone());
1949 }
1950
1951 if let Some(ref families) = custom.ip_families {
1953 default.ip_families = Some(families.clone());
1954 }
1955
1956 if let Some(ref policy) = custom.ip_family_policy {
1958 default.ip_family_policy = Some(policy.clone());
1959 }
1960
1961 if let Some(ref ips) = custom.cluster_ips {
1963 default.cluster_ips = Some(ips.clone());
1964 }
1965
1966 if let Some(ref custom_ports) = custom.ports {
1968 if let Some(ref mut default_ports) = default.ports {
1969 for custom_port in custom_ports {
1971 if let Some(existing_port) = default_ports
1972 .iter_mut()
1973 .find(|p| p.name == custom_port.name)
1974 {
1975 *existing_port = custom_port.clone();
1977 } else {
1978 default_ports.push(custom_port.clone());
1980 }
1981 }
1982 } else {
1983 default.ports = Some(custom_ports.clone());
1985 }
1986 }
1987
1988 }