1use crate::bind9_acl::build_acl_list;
10use crate::constants::{
11 API_GROUP_VERSION, BIND9_MALLOC_CONF, BIND9_NONROOT_UID, BIND9_SERVICE_ACCOUNT,
12 CONTAINER_NAME_BIND9, CONTAINER_NAME_BINDCAR, DEFAULT_BIND9_VERSION, DNS_CONTAINER_PORT,
13 DNS_PORT, KIND_BIND9_INSTANCE, LIVENESS_FAILURE_THRESHOLD, LIVENESS_INITIAL_DELAY_SECS,
14 LIVENESS_PERIOD_SECS, LIVENESS_TIMEOUT_SECS, READINESS_FAILURE_THRESHOLD,
15 READINESS_INITIAL_DELAY_SECS, READINESS_PERIOD_SECS, READINESS_TIMEOUT_SECS, RNDC_PORT,
16};
17use crate::crd::{Bind9Cluster, Bind9Instance, ConfigMapRefs, ImageConfig};
18use crate::labels::{
19 APP_NAME_BIND9, COMPONENT_DNS_CLUSTER, COMPONENT_DNS_SERVER, K8S_COMPONENT, K8S_INSTANCE,
20 K8S_MANAGED_BY, K8S_NAME, K8S_PART_OF, MANAGED_BY_BIND9_CLUSTER, MANAGED_BY_BIND9_INSTANCE,
21 PART_OF_BINDY,
22};
23use anyhow::Context;
24use k8s_openapi::api::{
25 apps::v1::{Deployment, DeploymentSpec},
26 core::v1::{
27 Capabilities, ConfigMap, Container, ContainerPort, EnvVar, EnvVarSource,
28 PodSecurityContext, PodSpec, PodTemplateSpec, Probe, SecretKeySelector, SecurityContext,
29 Service, ServiceAccount, ServicePort, ServiceSpec, TCPSocketAction, Volume, VolumeMount,
30 },
31};
32use k8s_openapi::apimachinery::pkg::{
33 apis::meta::v1::{LabelSelector, ObjectMeta, OwnerReference},
34 util::intstr::IntOrString,
35};
36use kube::ResourceExt;
37use std::collections::BTreeMap;
38use tracing::{debug, warn};
39
40const NAMED_CONF_TEMPLATE: &str = include_str!("../templates/named.conf.tmpl");
42const NAMED_CONF_OPTIONS_TEMPLATE: &str = include_str!("../templates/named.conf.options.tmpl");
43const RNDC_CONF_TEMPLATE: &str = include_str!("../templates/rndc.conf.tmpl");
44
45const DNSSEC_POLICY_TEMPLATE: &str = r#"
47dnssec-policy "{{POLICY_NAME}}" {
48 // Key configuration
49 keys {
50 ksk lifetime {{KSK_LIFETIME}} algorithm {{ALGORITHM}};
51 zsk lifetime {{ZSK_LIFETIME}} algorithm {{ALGORITHM}};
52 };
53
54 // Authenticated denial of existence
55 {{NSEC_CONFIG}};
56
57 // Signature validity periods
58 signatures-refresh 5d;
59 signatures-validity 30d;
60 signatures-validity-dnskey 30d;
61
62 // Zone propagation delay (time for zone updates to reach all servers)
63 zone-propagation-delay 300; // 5 minutes
64
65 // Parent propagation delay (time for DS updates in parent zone)
66 parent-propagation-delay 3600; // 1 hour
67
68 // Maximum zone TTL (affects key rollover timing)
69 max-zone-ttl 86400; // 24 hours
70};
71"#;
72
73const BIND_ZONES_PATH: &str = "/etc/bind/zones";
75const BIND_CACHE_PATH: &str = "/var/cache/bind";
76const BIND_KEYS_PATH: &str = "/etc/bind/keys";
77const BIND_DNSSEC_KEYS_PATH: &str = "/var/cache/bind/keys";
78const BIND_NAMED_CONF_PATH: &str = "/etc/bind/named.conf";
79const BIND_NAMED_CONF_OPTIONS_PATH: &str = "/etc/bind/named.conf.options";
80const BIND_NAMED_CONF_ZONES_PATH: &str = "/etc/bind/named.conf.zones";
81const BIND_RNDC_CONF_PATH: &str = "/etc/bind/rndc.conf";
82
83const NAMED_CONF_FILENAME: &str = "named.conf";
85const NAMED_CONF_OPTIONS_FILENAME: &str = "named.conf.options";
86const NAMED_CONF_ZONES_FILENAME: &str = "named.conf.zones";
87const RNDC_CONF_FILENAME: &str = "rndc.conf";
88
89const VOLUME_ZONES: &str = "zones";
91const VOLUME_CACHE: &str = "cache";
92const VOLUME_RNDC_KEY: &str = "rndc-key";
93const VOLUME_CONFIG: &str = "config";
94const VOLUME_NAMED_CONF: &str = "named-conf";
95const VOLUME_NAMED_CONF_OPTIONS: &str = "named-conf-options";
96const VOLUME_NAMED_CONF_ZONES: &str = "named-conf-zones";
97const VOLUME_DNSSEC_KEYS: &str = "dnssec-keys";
98
99const DEFAULT_DNSSEC_POLICY_NAME: &str = "default";
101const DEFAULT_DNSSEC_ALGORITHM: &str = "ECDSAP256SHA256";
102const DEFAULT_KSK_LIFETIME: &str = "unlimited";
103const DEFAULT_ZSK_LIFETIME: &str = "unlimited";
104const DEFAULT_NSEC3_SALT_LENGTH: u8 = 16;
105
106pub(crate) fn generate_dnssec_policies(
120 global_config: Option<&crate::crd::Bind9Config>,
121 instance_config: Option<&crate::crd::Bind9Config>,
122) -> String {
123 let dnssec_config = if let Some(instance) = instance_config {
125 instance.dnssec.as_ref().and_then(|d| d.signing.as_ref())
126 } else {
127 global_config.and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
128 };
129
130 let Some(signing) = dnssec_config else {
132 return String::new();
133 };
134
135 if !signing.enabled {
136 return String::new();
137 }
138
139 let policy_name = signing
141 .policy
142 .as_deref()
143 .unwrap_or(DEFAULT_DNSSEC_POLICY_NAME);
144 let algorithm = signing
145 .algorithm
146 .as_deref()
147 .unwrap_or(DEFAULT_DNSSEC_ALGORITHM);
148 let ksk_lifetime = signing
149 .ksk_lifetime
150 .as_deref()
151 .unwrap_or(DEFAULT_KSK_LIFETIME);
152 let zsk_lifetime = signing
153 .zsk_lifetime
154 .as_deref()
155 .unwrap_or(DEFAULT_ZSK_LIFETIME);
156
157 let nsec_config = if signing.nsec3.unwrap_or(false) {
159 let iterations = signing.nsec3_iterations.unwrap_or(0);
160 let salt_length = DEFAULT_NSEC3_SALT_LENGTH;
161 format!("nsec3param iterations {iterations} optout no salt-length {salt_length}")
162 } else {
163 "nsec".to_string()
164 };
165
166 DNSSEC_POLICY_TEMPLATE
168 .replace("{{POLICY_NAME}}", policy_name)
169 .replace("{{ALGORITHM}}", algorithm)
170 .replace("{{KSK_LIFETIME}}", ksk_lifetime)
171 .replace("{{ZSK_LIFETIME}}", zsk_lifetime)
172 .replace("{{NSEC_CONFIG}}", &nsec_config)
173}
174
175#[allow(dead_code)]
188pub(crate) fn is_dnssec_signing_enabled(
189 global_config: Option<&crate::crd::Bind9Config>,
190 instance_config: Option<&crate::crd::Bind9Config>,
191) -> bool {
192 let dnssec_config = if let Some(instance) = instance_config {
194 instance.dnssec.as_ref().and_then(|d| d.signing.as_ref())
195 } else {
196 global_config.and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
197 };
198
199 dnssec_config.is_some_and(|signing| signing.enabled)
200}
201
202pub(crate) fn get_dnssec_signing_config<'a>(
215 global_config: Option<&'a crate::crd::Bind9Config>,
216 instance_config: Option<&'a crate::crd::Bind9Config>,
217) -> Option<&'a crate::crd::DNSSECSigningConfig> {
218 if let Some(instance) = instance_config {
220 if let Some(config) = instance.dnssec.as_ref().and_then(|d| d.signing.as_ref()) {
221 if config.enabled {
222 return Some(config);
223 }
224 }
225 }
226
227 global_config
228 .and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
229 .filter(|config| config.enabled)
230}
231
232pub(crate) fn build_dnssec_key_volumes(
248 global_config: Option<&crate::crd::Bind9Config>,
249 instance_config: Option<&crate::crd::Bind9Config>,
250) -> (Vec<Volume>, Vec<VolumeMount>) {
251 use k8s_openapi::api::core::v1::{
252 EmptyDirVolumeSource, SecretVolumeSource, Volume, VolumeMount,
253 };
254
255 let Some(signing_config) = get_dnssec_signing_config(global_config, instance_config) else {
256 return (vec![], vec![]);
257 };
258
259 let mut volumes = Vec::new();
260 let mut volume_mounts = Vec::new();
261
262 match &signing_config.keys_from {
264 Some(crate::crd::DNSSECKeySource {
266 secret_ref: Some(secret),
267 ..
268 }) => {
269 volumes.push(Volume {
270 name: VOLUME_DNSSEC_KEYS.to_string(),
271 secret: Some(SecretVolumeSource {
272 secret_name: Some(secret.name.clone()),
273 default_mode: Some(0o600), ..Default::default()
275 }),
276 ..Default::default()
277 });
278
279 volume_mounts.push(VolumeMount {
280 name: VOLUME_DNSSEC_KEYS.to_string(),
281 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
282 read_only: Some(false), ..Default::default()
284 });
285
286 debug!(
287 secret_name = %secret.name,
288 "Mounting user-supplied DNSSEC keys from Secret"
289 );
290 }
291
292 None
295 | Some(crate::crd::DNSSECKeySource {
296 secret_ref: None,
297 persistent_volume: None,
298 }) => {
299 if signing_config.auto_generate.unwrap_or(true) {
300 volumes.push(Volume {
301 name: VOLUME_DNSSEC_KEYS.to_string(),
302 empty_dir: Some(EmptyDirVolumeSource::default()),
303 ..Default::default()
304 });
305
306 volume_mounts.push(VolumeMount {
307 name: VOLUME_DNSSEC_KEYS.to_string(),
308 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
309 ..Default::default()
310 });
311
312 debug!("DNSSEC keys will be auto-generated by BIND9 in emptyDir");
313
314 if signing_config.export_to_secret.unwrap_or(true) {
315 debug!("Auto-generated keys will be exported to Secret for backup/restore");
316 }
317 }
318 }
319
320 Some(crate::crd::DNSSECKeySource {
322 persistent_volume: Some(_pvc),
323 ..
324 }) => {
325 warn!("Persistent storage for DNSSEC keys is not yet implemented - using emptyDir");
326 volumes.push(Volume {
327 name: VOLUME_DNSSEC_KEYS.to_string(),
328 empty_dir: Some(EmptyDirVolumeSource::default()),
329 ..Default::default()
330 });
331
332 volume_mounts.push(VolumeMount {
333 name: VOLUME_DNSSEC_KEYS.to_string(),
334 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
335 ..Default::default()
336 });
337 }
338 }
339
340 (volumes, volume_mounts)
341}
342
343#[must_use]
369pub fn build_cluster_labels(cluster_name: &str) -> BTreeMap<String, String> {
370 let mut labels = BTreeMap::new();
371 labels.insert("app".into(), APP_NAME_BIND9.into());
372 labels.insert("cluster".into(), cluster_name.into());
373 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
374 labels.insert(K8S_INSTANCE.into(), cluster_name.into());
375 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_CLUSTER.into());
376 labels.insert(K8S_MANAGED_BY.into(), MANAGED_BY_BIND9_CLUSTER.into());
377 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
378 labels
379}
380
381#[must_use]
402pub fn build_labels_from_instance(
403 instance_name: &str,
404 instance: &Bind9Instance,
405) -> BTreeMap<String, String> {
406 use crate::labels::{BINDY_MANAGED_BY_LABEL, BINDY_ROLE_LABEL};
407
408 let mut labels = BTreeMap::new();
409 labels.insert("app".into(), APP_NAME_BIND9.into());
410 labels.insert("instance".into(), instance_name.into());
411 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
412 labels.insert(K8S_INSTANCE.into(), instance_name.into());
413 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
414 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
415
416 let managed_by = instance
419 .metadata
420 .labels
421 .as_ref()
422 .and_then(|labels| labels.get(BINDY_MANAGED_BY_LABEL))
423 .map_or(MANAGED_BY_BIND9_INSTANCE, String::as_str);
424
425 labels.insert(K8S_MANAGED_BY.into(), managed_by.into());
426
427 if let Some(instance_labels) = &instance.metadata.labels {
430 if let Some(role) = instance_labels.get(BINDY_ROLE_LABEL) {
431 labels.insert(BINDY_ROLE_LABEL.into(), role.clone());
432 }
433 }
434
435 labels
436}
437
438#[must_use]
451pub fn build_owner_references(instance: &Bind9Instance) -> Vec<OwnerReference> {
452 vec![OwnerReference {
453 api_version: API_GROUP_VERSION.to_string(),
454 kind: KIND_BIND9_INSTANCE.to_string(),
455 name: instance.name_any(),
456 uid: instance.metadata.uid.clone().unwrap_or_default(),
457 controller: Some(true),
458 block_owner_deletion: Some(true),
459 }]
460}
461
462pub fn build_configmap(
485 name: &str,
486 namespace: &str,
487 instance: &Bind9Instance,
488 cluster: Option<&Bind9Cluster>,
489 role_allow_transfer: Option<&Vec<String>>,
490) -> anyhow::Result<Option<ConfigMap>> {
491 debug!(
492 name = %name,
493 namespace = %namespace,
494 "Building ConfigMap for Bind9Instance"
495 );
496
497 let config_map_refs = instance
499 .spec
500 .config_map_refs
501 .as_ref()
502 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
503
504 if let Some(refs) = config_map_refs {
506 if refs.named_conf.is_some() || refs.named_conf_options.is_some() {
507 debug!(
508 named_conf_ref = ?refs.named_conf,
509 named_conf_options_ref = ?refs.named_conf_options,
510 "Custom ConfigMaps specified, skipping generation"
511 );
512 return Ok(None);
514 }
515 }
516
517 let mut data = BTreeMap::new();
519 let labels = build_labels_from_instance(name, instance);
520
521 let named_conf = build_named_conf(instance, cluster);
523 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
524
525 let options_conf = build_options_conf(instance, cluster, role_allow_transfer)?;
527 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
528
529 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
531
532 let owner_refs = build_owner_references(instance);
536
537 Ok(Some(ConfigMap {
538 metadata: ObjectMeta {
539 name: Some(format!("{name}-config")),
540 namespace: Some(namespace.into()),
541 labels: Some(labels),
542 owner_references: Some(owner_refs),
543 ..Default::default()
544 },
545 data: Some(data),
546 ..Default::default()
547 }))
548}
549
550pub fn build_cluster_configmap(
570 cluster_name: &str,
571 namespace: &str,
572 cluster: &Bind9Cluster,
573) -> Result<ConfigMap, anyhow::Error> {
574 debug!(
575 cluster_name = %cluster_name,
576 namespace = %namespace,
577 "Building cluster-level shared ConfigMap"
578 );
579
580 let mut data = BTreeMap::new();
582 let labels = build_cluster_labels(cluster_name);
583
584 let named_conf = build_cluster_named_conf(cluster);
586 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
587
588 let options_conf = build_cluster_options_conf(cluster)?;
590 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
591
592 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
594
595 Ok(ConfigMap {
596 metadata: ObjectMeta {
597 name: Some(format!("{cluster_name}-config")),
598 namespace: Some(namespace.into()),
599 labels: Some(labels),
600 ..Default::default()
601 },
602 data: Some(data),
603 ..Default::default()
604 })
605}
606
607fn build_named_conf(instance: &Bind9Instance, cluster: Option<&Bind9Cluster>) -> String {
621 let config_map_refs = instance
623 .spec
624 .config_map_refs
625 .as_ref()
626 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
627
628 let zones_include = if let Some(refs) = config_map_refs {
629 if refs.named_conf_zones.is_some() {
630 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
632 } else {
633 String::new()
635 }
636 } else {
637 String::new()
639 };
640
641 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
645 let rndc_key_names = "\"bindy-operator\"";
646
647 NAMED_CONF_TEMPLATE
648 .replace("{{ZONES_INCLUDE}}", &zones_include)
649 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
650 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
651}
652
653#[allow(clippy::too_many_lines)]
674fn build_options_conf(
675 instance: &Bind9Instance,
676 cluster: Option<&Bind9Cluster>,
677 role_allow_transfer: Option<&Vec<String>>,
678) -> anyhow::Result<String> {
679 let recursion;
680 let mut allow_query = String::new();
681 let allow_transfer;
682 let mut dnssec_validate = String::new();
683
684 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
686
687 if let Some(config) = &instance.spec.config {
688 let recursion_value = if let Some(rec) = config.recursion {
690 if rec {
691 "yes"
692 } else {
693 "no"
694 }
695 } else if let Some(global) = global_config {
696 if global.recursion.unwrap_or(false) {
697 "yes"
698 } else {
699 "no"
700 }
701 } else {
702 "no"
703 };
704 recursion = format!("recursion {recursion_value};");
705
706 if let Some(acls) = &config.allow_query {
708 if !acls.is_empty() {
709 let acl_list = build_acl_list(acls)
710 .context("invalid entry in instance spec.config.allow_query")?;
711 allow_query = format!("allow-query {{ {acl_list}; }};");
712 }
713 } else if let Some(global) = global_config {
714 if let Some(global_acls) = &global.allow_query {
715 if !global_acls.is_empty() {
716 let acl_list = build_acl_list(global_acls)
717 .context("invalid entry in cluster spec.global.allow_query")?;
718 allow_query = format!("allow-query {{ {acl_list}; }};");
719 }
720 }
721 }
722
723 if let Some(acls) = &config.allow_transfer {
725 let acl_list = if acls.is_empty() {
727 "none".to_string()
728 } else {
729 build_acl_list(acls)
730 .context("invalid entry in instance spec.config.allow_transfer")?
731 };
732 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
733 } else if let Some(role_acls) = role_allow_transfer {
734 let acl_list = if role_acls.is_empty() {
736 "none".to_string()
737 } else {
738 build_acl_list(role_acls)
739 .context("invalid entry in cluster role-specific allow_transfer")?
740 };
741 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
742 } else if let Some(global) = global_config {
743 if let Some(global_acls) = &global.allow_transfer {
745 let acl_list = if global_acls.is_empty() {
746 "none".to_string()
747 } else {
748 build_acl_list(global_acls)
749 .context("invalid entry in cluster spec.global.allow_transfer")?
750 };
751 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
752 } else {
753 allow_transfer = String::new();
754 }
755 } else {
756 allow_transfer = String::new();
758 }
759
760 if let Some(dnssec) = &config.dnssec {
764 if dnssec.validation.unwrap_or(false) {
765 dnssec_validate = "dnssec-validation yes;".to_string();
766 } else {
767 dnssec_validate = "dnssec-validation no;".to_string();
768 }
769 } else if let Some(global) = global_config {
770 if let Some(global_dnssec) = &global.dnssec {
771 if global_dnssec.validation.unwrap_or(false) {
772 dnssec_validate = "dnssec-validation yes;".to_string();
773 } else {
774 dnssec_validate = "dnssec-validation no;".to_string();
775 }
776 }
777 }
778 } else {
779 if let Some(global) = global_config {
781 let recursion_value = if global.recursion.unwrap_or(false) {
783 "yes"
784 } else {
785 "no"
786 };
787 recursion = format!("recursion {recursion_value};");
788
789 if let Some(acls) = &global.allow_query {
791 if !acls.is_empty() {
792 let acl_list = build_acl_list(acls)
793 .context("invalid entry in cluster spec.global.allow_query")?;
794 allow_query = format!("allow-query {{ {acl_list}; }};");
795 }
796 }
797
798 if let Some(role_acls) = role_allow_transfer {
800 let acl_list = if role_acls.is_empty() {
801 "none".to_string()
802 } else {
803 build_acl_list(role_acls)
804 .context("invalid entry in cluster role-specific allow_transfer")?
805 };
806 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
807 } else if let Some(global_acls) = &global.allow_transfer {
808 let acl_list = if global_acls.is_empty() {
809 "none".to_string()
810 } else {
811 build_acl_list(global_acls)
812 .context("invalid entry in cluster spec.global.allow_transfer")?
813 };
814 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
815 } else {
816 allow_transfer = String::new();
817 }
818
819 if let Some(dnssec) = &global.dnssec {
821 if dnssec.validation.unwrap_or(false) {
822 dnssec_validate = "dnssec-validation yes;".to_string();
823 }
824 }
825 } else {
826 recursion = "recursion no;".to_string();
828 allow_transfer = String::new();
830 }
831 }
832
833 let dnssec_policies = generate_dnssec_policies(global_config, instance.spec.config.as_ref());
835
836 Ok(NAMED_CONF_OPTIONS_TEMPLATE
838 .replace("{{RECURSION}}", &recursion)
839 .replace("{{ALLOW_QUERY}}", &allow_query)
840 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
841 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
842 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies))
843}
844
845fn build_cluster_named_conf(cluster: &Bind9Cluster) -> String {
858 let zones_include = if let Some(refs) = &cluster.spec.common.config_map_refs {
860 if refs.named_conf_zones.is_some() {
861 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
863 } else {
864 String::new()
866 }
867 } else {
868 String::new()
870 };
871
872 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
876 let rndc_key_names = "\"bindy-operator\"";
877
878 NAMED_CONF_TEMPLATE
879 .replace("{{ZONES_INCLUDE}}", &zones_include)
880 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
881 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
882}
883
884#[allow(clippy::too_many_lines)]
897fn build_cluster_options_conf(cluster: &Bind9Cluster) -> anyhow::Result<String> {
898 let recursion;
899 let mut allow_query = String::new();
900 let mut allow_transfer = String::new();
901 let mut dnssec_validate = String::new();
902
903 if let Some(global) = &cluster.spec.common.global {
905 let recursion_value = if global.recursion.unwrap_or(false) {
907 "yes"
908 } else {
909 "no"
910 };
911 recursion = format!("recursion {recursion_value};");
912
913 if let Some(aq) = &global.allow_query {
915 if !aq.is_empty() {
916 let acl_list = build_acl_list(aq)
917 .context("invalid entry in cluster spec.global.allow_query")?;
918 allow_query = format!("allow-query {{ {acl_list}; }};");
919 }
920 }
921
922 if let Some(at) = &global.allow_transfer {
924 if !at.is_empty() {
925 let acl_list = build_acl_list(at)
926 .context("invalid entry in cluster spec.global.allow_transfer")?;
927 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
928 }
929 }
930
931 if let Some(dnssec) = &global.dnssec {
933 if dnssec.validation.unwrap_or(false) {
934 dnssec_validate = "dnssec-validation yes;".to_string();
935 } else {
936 dnssec_validate = "dnssec-validation no;".to_string();
937 }
938 }
939 } else {
940 recursion = "recursion no;".to_string();
942 }
943
944 let dnssec_policies = generate_dnssec_policies(cluster.spec.common.global.as_ref(), None);
946
947 Ok(NAMED_CONF_OPTIONS_TEMPLATE
948 .replace("{{RECURSION}}", &recursion)
949 .replace("{{ALLOW_QUERY}}", &allow_query)
950 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
951 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
952 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies))
953}
954
955#[must_use]
975struct DeploymentConfig<'a> {
977 image_config: Option<&'a ImageConfig>,
978 config_map_refs: Option<&'a ConfigMapRefs>,
979 version: &'a str,
980 volumes: Option<&'a Vec<Volume>>,
981 volume_mounts: Option<&'a Vec<VolumeMount>>,
982 bindcar_config: Option<&'a crate::crd::BindcarConfig>,
983 configmap_name: String,
984 rndc_secret_name: String,
985}
986
987fn resolve_deployment_config<'a>(
989 name: &str,
990 instance: &'a Bind9Instance,
991 cluster: Option<&'a Bind9Cluster>,
992 cluster_provider: Option<&'a crate::crd::ClusterBind9Provider>,
993) -> DeploymentConfig<'a> {
994 let image_config = instance
996 .spec
997 .image
998 .as_ref()
999 .or_else(|| cluster.and_then(|c| c.spec.common.image.as_ref()))
1000 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.image.as_ref()));
1001
1002 let config_map_refs = instance
1004 .spec
1005 .config_map_refs
1006 .as_ref()
1007 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()))
1008 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.config_map_refs.as_ref()));
1009
1010 let version = instance
1012 .spec
1013 .version
1014 .as_deref()
1015 .or_else(|| cluster.and_then(|c| c.spec.common.version.as_deref()))
1016 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.version.as_deref()))
1017 .unwrap_or(DEFAULT_BIND9_VERSION);
1018
1019 let volumes = instance
1021 .spec
1022 .volumes
1023 .as_ref()
1024 .or_else(|| cluster.and_then(|c| c.spec.common.volumes.as_ref()))
1025 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volumes.as_ref()));
1026
1027 let volume_mounts = instance
1029 .spec
1030 .volume_mounts
1031 .as_ref()
1032 .or_else(|| cluster.and_then(|c| c.spec.common.volume_mounts.as_ref()))
1033 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volume_mounts.as_ref()));
1034
1035 let bindcar_config = instance
1037 .spec
1038 .bindcar_config
1039 .as_ref()
1040 .or_else(|| {
1041 cluster.and_then(|c| {
1042 c.spec
1043 .common
1044 .global
1045 .as_ref()
1046 .and_then(|g| g.bindcar_config.as_ref())
1047 })
1048 })
1049 .or_else(|| {
1050 cluster_provider.and_then(|cp| {
1051 cp.spec
1052 .common
1053 .global
1054 .as_ref()
1055 .and_then(|g| g.bindcar_config.as_ref())
1056 })
1057 });
1058
1059 let configmap_name = if instance.spec.cluster_ref.is_empty() {
1061 format!("{name}-config")
1063 } else {
1064 format!("{}-config", instance.spec.cluster_ref)
1066 };
1067
1068 let rndc_secret_name = format!("{name}-rndc-key");
1071
1072 DeploymentConfig {
1073 image_config,
1074 config_map_refs,
1075 version,
1076 volumes,
1077 volume_mounts,
1078 bindcar_config,
1079 configmap_name,
1080 rndc_secret_name,
1081 }
1082}
1083
1084pub fn build_deployment(
1085 name: &str,
1086 namespace: &str,
1087 instance: &Bind9Instance,
1088 cluster: Option<&Bind9Cluster>,
1089 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1090) -> Deployment {
1091 debug!(
1092 name = %name,
1093 namespace = %namespace,
1094 has_cluster = cluster.is_some(),
1095 has_cluster_provider = cluster_provider.is_some(),
1096 "Building Deployment for Bind9Instance"
1097 );
1098
1099 let labels = build_labels_from_instance(name, instance);
1101 let replicas = instance.spec.replicas.unwrap_or(1);
1102 debug!(replicas, "Deployment replica count");
1103
1104 let config = resolve_deployment_config(name, instance, cluster, cluster_provider);
1105
1106 let owner_refs = build_owner_references(instance);
1107
1108 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
1110 let instance_config = instance.spec.config.as_ref();
1111
1112 let (dnssec_volumes, dnssec_volume_mounts) =
1114 build_dnssec_key_volumes(global_config, instance_config);
1115
1116 let all_volumes = if dnssec_volumes.is_empty() {
1118 config.volumes.map(std::borrow::ToOwned::to_owned)
1119 } else {
1120 let mut merged = dnssec_volumes;
1121 if let Some(custom) = config.volumes {
1122 merged.extend(custom.iter().cloned());
1123 }
1124 Some(merged)
1125 };
1126
1127 let all_volume_mounts = if dnssec_volume_mounts.is_empty() {
1129 config.volume_mounts.map(std::borrow::ToOwned::to_owned)
1130 } else {
1131 let mut merged = dnssec_volume_mounts;
1132 if let Some(custom) = config.volume_mounts {
1133 merged.extend(custom.iter().cloned());
1134 }
1135 Some(merged)
1136 };
1137
1138 Deployment {
1139 metadata: ObjectMeta {
1140 name: Some(name.into()),
1141 namespace: Some(namespace.into()),
1142 labels: Some(labels.clone()),
1143 owner_references: Some(owner_refs),
1144 ..Default::default()
1145 },
1146 spec: Some(DeploymentSpec {
1147 replicas: Some(replicas),
1148 selector: LabelSelector {
1149 match_labels: Some(labels.clone()),
1150 ..Default::default()
1151 },
1152 template: PodTemplateSpec {
1153 metadata: Some(ObjectMeta {
1154 labels: Some(labels.clone()),
1155 ..Default::default()
1156 }),
1157 spec: Some(build_pod_spec(
1158 namespace,
1159 &config.configmap_name,
1160 &config.rndc_secret_name,
1161 config.version,
1162 config.image_config,
1163 config.config_map_refs,
1164 all_volumes.as_ref(),
1165 all_volume_mounts.as_ref(),
1166 config.bindcar_config,
1167 )),
1168 },
1169 ..Default::default()
1170 }),
1171 ..Default::default()
1172 }
1173}
1174
1175#[allow(clippy::too_many_arguments)]
1188#[allow(clippy::too_many_lines)]
1189fn build_pod_spec(
1190 namespace: &str,
1191 configmap_name: &str,
1192 rndc_secret_name: &str,
1193 version: &str,
1194 image_config: Option<&ImageConfig>,
1195 config_map_refs: Option<&ConfigMapRefs>,
1196 custom_volumes: Option<&Vec<Volume>>,
1197 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1198 bindcar_config: Option<&crate::crd::BindcarConfig>,
1199) -> PodSpec {
1200 let image = if let Some(img_cfg) = image_config {
1202 img_cfg
1203 .image
1204 .clone()
1205 .unwrap_or_else(|| format!("internetsystemsconsortium/bind9:{version}"))
1206 } else {
1207 format!("internetsystemsconsortium/bind9:{version}")
1208 };
1209
1210 let image_pull_policy = image_config
1212 .and_then(|cfg| cfg.image_pull_policy.clone())
1213 .unwrap_or_else(|| "IfNotPresent".into());
1214
1215 let bind9_container = Container {
1217 name: CONTAINER_NAME_BIND9.into(),
1218 image: Some(image),
1219 image_pull_policy: Some(image_pull_policy),
1220 command: Some(vec!["named".into()]),
1221 args: Some(vec![
1222 "-c".into(),
1223 BIND_NAMED_CONF_PATH.into(),
1224 "-g".into(), ]),
1226 ports: Some(vec![
1227 ContainerPort {
1228 name: Some("dns-tcp".into()),
1229 container_port: i32::from(DNS_CONTAINER_PORT),
1230 protocol: Some("TCP".into()),
1231 ..Default::default()
1232 },
1233 ContainerPort {
1234 name: Some("dns-udp".into()),
1235 container_port: i32::from(DNS_CONTAINER_PORT),
1236 protocol: Some("UDP".into()),
1237 ..Default::default()
1238 },
1239 ContainerPort {
1240 name: Some("rndc".into()),
1241 container_port: i32::from(RNDC_PORT),
1242 protocol: Some("TCP".into()),
1243 ..Default::default()
1244 },
1245 ]),
1246 env: Some(vec![
1247 EnvVar {
1248 name: "TZ".into(),
1249 value: Some("UTC".into()),
1250 ..Default::default()
1251 },
1252 EnvVar {
1253 name: "MALLOC_CONF".into(),
1254 value: Some(BIND9_MALLOC_CONF.into()),
1255 ..Default::default()
1256 },
1257 ]),
1258 volume_mounts: Some(build_volume_mounts(config_map_refs, custom_volume_mounts)),
1259 liveness_probe: Some(Probe {
1260 tcp_socket: Some(TCPSocketAction {
1261 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1262 ..Default::default()
1263 }),
1264 initial_delay_seconds: Some(LIVENESS_INITIAL_DELAY_SECS),
1265 period_seconds: Some(LIVENESS_PERIOD_SECS),
1266 timeout_seconds: Some(LIVENESS_TIMEOUT_SECS),
1267 failure_threshold: Some(LIVENESS_FAILURE_THRESHOLD),
1268 ..Default::default()
1269 }),
1270 readiness_probe: Some(Probe {
1271 tcp_socket: Some(TCPSocketAction {
1272 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1273 ..Default::default()
1274 }),
1275 initial_delay_seconds: Some(READINESS_INITIAL_DELAY_SECS),
1276 period_seconds: Some(READINESS_PERIOD_SECS),
1277 timeout_seconds: Some(READINESS_TIMEOUT_SECS),
1278 failure_threshold: Some(READINESS_FAILURE_THRESHOLD),
1279 ..Default::default()
1280 }),
1281 security_context: Some(SecurityContext {
1282 run_as_non_root: Some(true),
1283 run_as_user: Some(BIND9_NONROOT_UID),
1284 run_as_group: Some(BIND9_NONROOT_UID),
1285 allow_privilege_escalation: Some(false),
1286 capabilities: Some(Capabilities {
1287 drop: Some(vec!["ALL".to_string()]),
1288 ..Default::default()
1289 }),
1290 ..Default::default()
1291 }),
1292 ..Default::default()
1293 };
1294
1295 let image_pull_secrets = image_config.and_then(|cfg| {
1297 cfg.image_pull_secrets.as_ref().map(|secrets| {
1298 secrets
1299 .iter()
1300 .map(|s| k8s_openapi::api::core::v1::LocalObjectReference { name: s.clone() })
1301 .collect()
1302 })
1303 });
1304
1305 PodSpec {
1306 containers: {
1307 let mut containers = vec![bind9_container];
1308 containers.push(build_api_sidecar_container(
1309 namespace,
1310 bindcar_config,
1311 rndc_secret_name,
1312 ));
1313 containers
1314 },
1315 volumes: Some(build_volumes(
1316 configmap_name,
1317 rndc_secret_name,
1318 config_map_refs,
1319 custom_volumes,
1320 )),
1321 image_pull_secrets,
1322 service_account_name: Some(BIND9_SERVICE_ACCOUNT.into()),
1323 security_context: Some(PodSecurityContext {
1324 run_as_user: Some(BIND9_NONROOT_UID),
1325 run_as_group: Some(BIND9_NONROOT_UID),
1326 fs_group: Some(BIND9_NONROOT_UID),
1327 run_as_non_root: Some(true),
1328 ..Default::default()
1329 }),
1330 ..Default::default()
1331 }
1332}
1333
1334#[allow(clippy::too_many_lines)]
1346fn build_api_sidecar_container(
1347 namespace: &str,
1348 bindcar_config: Option<&crate::crd::BindcarConfig>,
1349 rndc_secret_name: &str,
1350) -> Container {
1351 let image = bindcar_config
1353 .and_then(|c| c.image.clone())
1354 .unwrap_or_else(|| crate::constants::DEFAULT_BINDCAR_IMAGE.to_string());
1355
1356 let image_pull_policy = bindcar_config
1357 .and_then(|c| c.image_pull_policy.clone())
1358 .unwrap_or_else(|| "IfNotPresent".to_string());
1359
1360 let port = bindcar_config
1361 .and_then(|c| c.port)
1362 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
1363
1364 let log_level = bindcar_config
1365 .and_then(|c| c.log_level.clone())
1366 .unwrap_or_else(|| "info".to_string());
1367
1368 let resources = bindcar_config.and_then(|c| c.resources.clone());
1369
1370 let mut env_vars = vec![
1372 EnvVar {
1373 name: "BIND_ZONE_DIR".into(),
1374 value: Some(BIND_CACHE_PATH.into()),
1375 ..Default::default()
1376 },
1377 EnvVar {
1378 name: "API_PORT".into(),
1379 value: Some(port.to_string()),
1380 ..Default::default()
1381 },
1382 EnvVar {
1383 name: "RUST_LOG".into(),
1384 value: Some(log_level),
1385 ..Default::default()
1386 },
1387 EnvVar {
1388 name: "BIND_ALLOWED_SERVICE_ACCOUNTS".into(),
1389 value: Some(format!(
1390 "system:serviceaccount:{namespace}:{BIND9_SERVICE_ACCOUNT}"
1391 )),
1392 ..Default::default()
1393 },
1394 EnvVar {
1395 name: "RNDC_SECRET".into(),
1396 value_from: Some(EnvVarSource {
1397 secret_key_ref: Some(SecretKeySelector {
1398 name: rndc_secret_name.to_string(),
1399 key: "secret".to_string(),
1400 optional: Some(false),
1401 }),
1402 ..Default::default()
1403 }),
1404 ..Default::default()
1405 },
1406 EnvVar {
1407 name: "RNDC_ALGORITHM".into(),
1408 value_from: Some(EnvVarSource {
1409 secret_key_ref: Some(SecretKeySelector {
1410 name: rndc_secret_name.to_string(),
1411 key: "algorithm".to_string(),
1412 optional: Some(false),
1413 }),
1414 ..Default::default()
1415 }),
1416 ..Default::default()
1417 },
1418 ];
1419
1420 if let Some(config) = bindcar_config {
1422 if let Some(user_env_vars) = &config.env_vars {
1423 env_vars.extend(user_env_vars.clone());
1424 }
1425 }
1426
1427 Container {
1428 name: CONTAINER_NAME_BINDCAR.into(),
1429 image: Some(image),
1430 image_pull_policy: Some(image_pull_policy),
1431 ports: Some(vec![ContainerPort {
1432 name: Some("http".into()),
1433 container_port: port,
1434 protocol: Some("TCP".into()),
1435 ..Default::default()
1436 }]),
1437 env: Some(env_vars),
1438 volume_mounts: Some(vec![
1439 VolumeMount {
1440 name: "cache".into(),
1441 mount_path: BIND_CACHE_PATH.into(),
1442 ..Default::default()
1443 },
1444 VolumeMount {
1445 name: "rndc-key".into(),
1446 mount_path: BIND_KEYS_PATH.into(),
1447 read_only: Some(true),
1448 ..Default::default()
1449 },
1450 VolumeMount {
1451 name: VOLUME_CONFIG.into(),
1452 mount_path: BIND_RNDC_CONF_PATH.into(),
1453 sub_path: Some(RNDC_CONF_FILENAME.into()),
1454 ..Default::default()
1455 },
1456 ]),
1457 resources,
1458 security_context: Some(SecurityContext {
1459 run_as_non_root: Some(true),
1460 run_as_user: Some(BIND9_NONROOT_UID),
1461 run_as_group: Some(BIND9_NONROOT_UID),
1462 allow_privilege_escalation: Some(false),
1463 capabilities: Some(Capabilities {
1464 drop: Some(vec!["ALL".to_string()]),
1465 ..Default::default()
1466 }),
1467 ..Default::default()
1468 }),
1469 ..Default::default()
1470 }
1471}
1472
1473fn build_volume_mounts(
1491 config_map_refs: Option<&ConfigMapRefs>,
1492 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1493) -> Vec<VolumeMount> {
1494 let mut mounts = vec![
1495 VolumeMount {
1496 name: VOLUME_ZONES.into(),
1497 mount_path: BIND_ZONES_PATH.into(),
1498 ..Default::default()
1499 },
1500 VolumeMount {
1501 name: VOLUME_CACHE.into(),
1502 mount_path: BIND_CACHE_PATH.into(),
1503 ..Default::default()
1504 },
1505 VolumeMount {
1506 name: VOLUME_RNDC_KEY.into(),
1507 mount_path: BIND_KEYS_PATH.into(),
1508 read_only: Some(true),
1509 ..Default::default()
1510 },
1511 ];
1512
1513 if let Some(refs) = config_map_refs {
1515 if let Some(_configmap_name) = &refs.named_conf {
1516 mounts.push(VolumeMount {
1517 name: VOLUME_NAMED_CONF.into(),
1518 mount_path: BIND_NAMED_CONF_PATH.into(),
1519 sub_path: Some(NAMED_CONF_FILENAME.into()),
1520 ..Default::default()
1521 });
1522 } else {
1523 mounts.push(VolumeMount {
1525 name: VOLUME_CONFIG.into(),
1526 mount_path: BIND_NAMED_CONF_PATH.into(),
1527 sub_path: Some(NAMED_CONF_FILENAME.into()),
1528 ..Default::default()
1529 });
1530 }
1531
1532 if let Some(_configmap_name) = &refs.named_conf_options {
1533 mounts.push(VolumeMount {
1534 name: VOLUME_NAMED_CONF_OPTIONS.into(),
1535 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1536 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1537 ..Default::default()
1538 });
1539 } else {
1540 mounts.push(VolumeMount {
1542 name: VOLUME_CONFIG.into(),
1543 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1544 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1545 ..Default::default()
1546 });
1547 }
1548
1549 if let Some(_configmap_name) = &refs.named_conf_zones {
1551 mounts.push(VolumeMount {
1552 name: VOLUME_NAMED_CONF_ZONES.into(),
1553 mount_path: BIND_NAMED_CONF_ZONES_PATH.into(),
1554 sub_path: Some(NAMED_CONF_ZONES_FILENAME.into()),
1555 ..Default::default()
1556 });
1557 }
1558 } else {
1560 mounts.push(VolumeMount {
1562 name: VOLUME_CONFIG.into(),
1563 mount_path: BIND_NAMED_CONF_PATH.into(),
1564 sub_path: Some(NAMED_CONF_FILENAME.into()),
1565 ..Default::default()
1566 });
1567 mounts.push(VolumeMount {
1568 name: VOLUME_CONFIG.into(),
1569 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1570 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1571 ..Default::default()
1572 });
1573 }
1575
1576 mounts.push(VolumeMount {
1578 name: VOLUME_CONFIG.into(),
1579 mount_path: BIND_RNDC_CONF_PATH.into(),
1580 sub_path: Some(RNDC_CONF_FILENAME.into()),
1581 ..Default::default()
1582 });
1583
1584 if let Some(custom_mounts) = custom_volume_mounts {
1586 mounts.extend(custom_mounts.iter().cloned());
1587 }
1588
1589 mounts
1590}
1591
1592fn build_volumes(
1613 configmap_name: &str,
1614 rndc_secret_name: &str,
1615 config_map_refs: Option<&ConfigMapRefs>,
1616 custom_volumes: Option<&Vec<Volume>>,
1617) -> Vec<Volume> {
1618 let mut volumes = vec![
1619 Volume {
1620 name: VOLUME_ZONES.into(),
1621 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
1622 ..Default::default()
1623 },
1624 Volume {
1625 name: VOLUME_CACHE.into(),
1626 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
1627 ..Default::default()
1628 },
1629 Volume {
1630 name: VOLUME_RNDC_KEY.into(),
1631 secret: Some(k8s_openapi::api::core::v1::SecretVolumeSource {
1632 secret_name: Some(rndc_secret_name.to_string()),
1633 ..Default::default()
1634 }),
1635 ..Default::default()
1636 },
1637 ];
1638
1639 if let Some(refs) = config_map_refs {
1641 if let Some(configmap_name) = &refs.named_conf {
1642 volumes.push(Volume {
1643 name: VOLUME_NAMED_CONF.into(),
1644 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1645 name: configmap_name.clone(),
1646 ..Default::default()
1647 }),
1648 ..Default::default()
1649 });
1650 }
1651
1652 if let Some(configmap_name) = &refs.named_conf_options {
1653 volumes.push(Volume {
1654 name: VOLUME_NAMED_CONF_OPTIONS.into(),
1655 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1656 name: configmap_name.clone(),
1657 ..Default::default()
1658 }),
1659 ..Default::default()
1660 });
1661 }
1662
1663 if let Some(configmap_name) = &refs.named_conf_zones {
1664 volumes.push(Volume {
1665 name: VOLUME_NAMED_CONF_ZONES.into(),
1666 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1667 name: configmap_name.clone(),
1668 ..Default::default()
1669 }),
1670 ..Default::default()
1671 });
1672 }
1673
1674 if refs.named_conf.is_none() || refs.named_conf_options.is_none() {
1677 volumes.push(Volume {
1678 name: VOLUME_CONFIG.into(),
1679 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1680 name: configmap_name.to_string(),
1681 ..Default::default()
1682 }),
1683 ..Default::default()
1684 });
1685 }
1686 } else {
1687 volumes.push(Volume {
1689 name: VOLUME_CONFIG.into(),
1690 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1691 name: configmap_name.to_string(),
1692 ..Default::default()
1693 }),
1694 ..Default::default()
1695 });
1696 }
1697
1698 if let Some(custom_vols) = custom_volumes {
1700 volumes.extend(custom_vols.iter().cloned());
1701 }
1702
1703 volumes
1704}
1705
1706#[must_use]
1748pub fn build_service(
1749 name: &str,
1750 namespace: &str,
1751 instance: &Bind9Instance,
1752 custom_config: Option<&crate::crd::ServiceConfig>,
1753) -> Service {
1754 let labels = build_labels_from_instance(name, instance);
1756 let owner_refs = build_owner_references(instance);
1757
1758 let api_container_port = instance
1760 .spec
1761 .bindcar_config
1762 .as_ref()
1763 .and_then(|c| c.port)
1764 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
1765
1766 let mut default_spec = ServiceSpec {
1768 selector: Some(labels.clone()),
1769 ports: Some(vec![
1770 ServicePort {
1771 name: Some("dns-tcp".into()),
1772 port: i32::from(DNS_PORT),
1773 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
1774 protocol: Some("TCP".into()),
1775 ..Default::default()
1776 },
1777 ServicePort {
1778 name: Some("dns-udp".into()),
1779 port: i32::from(DNS_PORT),
1780 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
1781 protocol: Some("UDP".into()),
1782 ..Default::default()
1783 },
1784 ServicePort {
1785 name: Some("http".into()),
1786 port: i32::from(crate::constants::BINDCAR_SERVICE_PORT),
1787 target_port: Some(IntOrString::Int(api_container_port)),
1788 protocol: Some("TCP".into()),
1789 ..Default::default()
1790 },
1791 ]),
1792 type_: Some("ClusterIP".into()),
1793 ..Default::default()
1794 };
1795
1796 if let Some(bindcar_service_spec) = instance
1798 .spec
1799 .bindcar_config
1800 .as_ref()
1801 .and_then(|c| c.service_spec.as_ref())
1802 {
1803 merge_service_spec(&mut default_spec, bindcar_service_spec);
1804 }
1805
1806 let (custom_spec, custom_annotations) = custom_config.map_or((None, None), |config| {
1808 (config.spec.as_ref(), config.annotations.as_ref())
1809 });
1810
1811 if let Some(custom) = custom_spec {
1813 merge_service_spec(&mut default_spec, custom);
1814 }
1815
1816 let mut metadata = ObjectMeta {
1818 name: Some(name.into()),
1819 namespace: Some(namespace.into()),
1820 labels: Some(labels),
1821 owner_references: Some(owner_refs),
1822 ..Default::default()
1823 };
1824
1825 if let Some(annotations) = custom_annotations {
1827 metadata.annotations = Some(annotations.clone());
1828 }
1829
1830 Service {
1831 metadata,
1832 spec: Some(default_spec),
1833 ..Default::default()
1834 }
1835}
1836
1837#[must_use]
1864pub fn build_service_account(namespace: &str, _instance: &Bind9Instance) -> ServiceAccount {
1865 let mut labels = BTreeMap::new();
1872 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
1873 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
1874 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
1875
1876 ServiceAccount {
1877 metadata: ObjectMeta {
1878 name: Some(BIND9_SERVICE_ACCOUNT.into()),
1879 namespace: Some(namespace.into()),
1880 labels: Some(labels),
1881 owner_references: None, ..Default::default()
1883 },
1884 ..Default::default()
1885 }
1886}
1887
1888fn merge_service_spec(default: &mut ServiceSpec, custom: &ServiceSpec) {
1896 if let Some(ref type_) = custom.type_ {
1898 default.type_ = Some(type_.clone());
1899 }
1900
1901 if let Some(ref lb_ip) = custom.load_balancer_ip {
1903 default.load_balancer_ip = Some(lb_ip.clone());
1904 }
1905
1906 if let Some(ref affinity) = custom.session_affinity {
1908 default.session_affinity = Some(affinity.clone());
1909 }
1910
1911 if let Some(ref config) = custom.session_affinity_config {
1913 default.session_affinity_config = Some(config.clone());
1914 }
1915
1916 if let Some(ref cluster_ip) = custom.cluster_ip {
1918 default.cluster_ip = Some(cluster_ip.clone());
1919 }
1920
1921 if let Some(ref policy) = custom.external_traffic_policy {
1923 default.external_traffic_policy = Some(policy.clone());
1924 }
1925
1926 if let Some(ref ranges) = custom.load_balancer_source_ranges {
1928 default.load_balancer_source_ranges = Some(ranges.clone());
1929 }
1930
1931 if let Some(ref ips) = custom.external_ips {
1933 default.external_ips = Some(ips.clone());
1934 }
1935
1936 if let Some(ref class) = custom.load_balancer_class {
1938 default.load_balancer_class = Some(class.clone());
1939 }
1940
1941 if let Some(port) = custom.health_check_node_port {
1943 default.health_check_node_port = Some(port);
1944 }
1945
1946 if let Some(publish) = custom.publish_not_ready_addresses {
1948 default.publish_not_ready_addresses = Some(publish);
1949 }
1950
1951 if let Some(allocate) = custom.allocate_load_balancer_node_ports {
1953 default.allocate_load_balancer_node_ports = Some(allocate);
1954 }
1955
1956 if let Some(ref policy) = custom.internal_traffic_policy {
1958 default.internal_traffic_policy = Some(policy.clone());
1959 }
1960
1961 if let Some(ref families) = custom.ip_families {
1963 default.ip_families = Some(families.clone());
1964 }
1965
1966 if let Some(ref policy) = custom.ip_family_policy {
1968 default.ip_family_policy = Some(policy.clone());
1969 }
1970
1971 if let Some(ref ips) = custom.cluster_ips {
1973 default.cluster_ips = Some(ips.clone());
1974 }
1975
1976 if let Some(ref custom_ports) = custom.ports {
1978 if let Some(ref mut default_ports) = default.ports {
1979 for custom_port in custom_ports {
1981 if let Some(existing_port) = default_ports
1982 .iter_mut()
1983 .find(|p| p.name == custom_port.name)
1984 {
1985 *existing_port = custom_port.clone();
1987 } else {
1988 default_ports.push(custom_port.clone());
1990 }
1991 }
1992 } else {
1993 default.ports = Some(custom_ports.clone());
1995 }
1996 }
1997
1998 }