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, EmptyDirVolumeSource, EnvVar,
28 EnvVarSource, PodSecurityContext, PodSpec, PodTemplateSpec, Probe, SeccompProfile,
29 SecretKeySelector, SecurityContext, Service, ServiceAccount, ServicePort, ServiceSpec,
30 TCPSocketAction, Volume, VolumeMount,
31 },
32};
33use k8s_openapi::apimachinery::pkg::{
34 apis::meta::v1::{LabelSelector, ObjectMeta, OwnerReference},
35 util::intstr::IntOrString,
36};
37use kube::ResourceExt;
38use std::collections::BTreeMap;
39use tracing::{debug, warn};
40
41const NAMED_CONF_TEMPLATE: &str = include_str!("../templates/named.conf.tmpl");
43const NAMED_CONF_OPTIONS_TEMPLATE: &str = include_str!("../templates/named.conf.options.tmpl");
44const RNDC_CONF_TEMPLATE: &str = include_str!("../templates/rndc.conf.tmpl");
45
46const DNSSEC_POLICY_TEMPLATE: &str = r#"
48dnssec-policy "{{POLICY_NAME}}" {
49 // Key configuration
50 keys {
51 ksk lifetime {{KSK_LIFETIME}} algorithm {{ALGORITHM}};
52 zsk lifetime {{ZSK_LIFETIME}} algorithm {{ALGORITHM}};
53 };
54{{NSEC_CONFIG}}
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";
96const VOLUME_TMP: &str = "tmp";
101
102const LISTEN_ON_DIRECTIVE: &str = "listen-on";
104const LISTEN_ON_V6_DIRECTIVE: &str = "listen-on-v6";
105const LISTEN_ON_DEFAULT: &str = "any";
107
108const DEFAULT_DNSSEC_POLICY_NAME: &str = "default";
110const DEFAULT_DNSSEC_ALGORITHM: &str = "ECDSAP256SHA256";
111const DEFAULT_KSK_LIFETIME: &str = "unlimited";
112const DEFAULT_ZSK_LIFETIME: &str = "unlimited";
113const DEFAULT_NSEC3_SALT_LENGTH: u8 = 16;
114
115pub(crate) fn generate_dnssec_policies(
129 global_config: Option<&crate::crd::Bind9Config>,
130 instance_config: Option<&crate::crd::Bind9Config>,
131) -> String {
132 let Some(signing) = get_dnssec_signing_config(global_config, instance_config) else {
137 return String::new();
138 };
139
140 let policy_name = signing
142 .policy
143 .as_deref()
144 .unwrap_or(DEFAULT_DNSSEC_POLICY_NAME);
145 let algorithm = signing
146 .algorithm
147 .as_deref()
148 .unwrap_or(DEFAULT_DNSSEC_ALGORITHM);
149 let ksk_lifetime = signing
150 .ksk_lifetime
151 .as_deref()
152 .unwrap_or(DEFAULT_KSK_LIFETIME);
153 let zsk_lifetime = signing
154 .zsk_lifetime
155 .as_deref()
156 .unwrap_or(DEFAULT_ZSK_LIFETIME);
157
158 let nsec_config = if signing.nsec3.unwrap_or(false) {
162 let iterations = signing.nsec3_iterations.unwrap_or(0);
163 let salt_length = DEFAULT_NSEC3_SALT_LENGTH;
164 format!(
165 "\n // Authenticated denial of existence (NSEC3)\n nsec3param iterations {iterations} optout no salt-length {salt_length};\n"
166 )
167 } else {
168 String::new()
169 };
170
171 DNSSEC_POLICY_TEMPLATE
173 .replace("{{POLICY_NAME}}", policy_name)
174 .replace("{{ALGORITHM}}", algorithm)
175 .replace("{{KSK_LIFETIME}}", ksk_lifetime)
176 .replace("{{ZSK_LIFETIME}}", zsk_lifetime)
177 .replace("{{NSEC_CONFIG}}", &nsec_config)
178}
179
180#[allow(dead_code)]
193pub(crate) fn is_dnssec_signing_enabled(
194 global_config: Option<&crate::crd::Bind9Config>,
195 instance_config: Option<&crate::crd::Bind9Config>,
196) -> bool {
197 let dnssec_config = if let Some(instance) = instance_config {
199 instance.dnssec.as_ref().and_then(|d| d.signing.as_ref())
200 } else {
201 global_config.and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
202 };
203
204 dnssec_config.is_some_and(|signing| signing.enabled)
205}
206
207pub(crate) fn get_dnssec_signing_config<'a>(
220 global_config: Option<&'a crate::crd::Bind9Config>,
221 instance_config: Option<&'a crate::crd::Bind9Config>,
222) -> Option<&'a crate::crd::DNSSECSigningConfig> {
223 if let Some(instance) = instance_config {
225 if let Some(config) = instance.dnssec.as_ref().and_then(|d| d.signing.as_ref()) {
226 if config.enabled {
227 return Some(config);
228 }
229 }
230 }
231
232 global_config
233 .and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
234 .filter(|config| config.enabled)
235}
236
237pub(crate) fn build_dnssec_key_volumes(
253 global_config: Option<&crate::crd::Bind9Config>,
254 instance_config: Option<&crate::crd::Bind9Config>,
255) -> (Vec<Volume>, Vec<VolumeMount>) {
256 use k8s_openapi::api::core::v1::{
257 EmptyDirVolumeSource, SecretVolumeSource, Volume, VolumeMount,
258 };
259
260 let Some(signing_config) = get_dnssec_signing_config(global_config, instance_config) else {
261 return (vec![], vec![]);
262 };
263
264 let mut volumes = Vec::new();
265 let mut volume_mounts = Vec::new();
266
267 match &signing_config.keys_from {
269 Some(crate::crd::DNSSECKeySource {
271 secret_ref: Some(secret),
272 ..
273 }) => {
274 volumes.push(Volume {
275 name: VOLUME_DNSSEC_KEYS.to_string(),
276 secret: Some(SecretVolumeSource {
277 secret_name: Some(secret.name.clone()),
278 default_mode: Some(0o600), ..Default::default()
280 }),
281 ..Default::default()
282 });
283
284 volume_mounts.push(VolumeMount {
285 name: VOLUME_DNSSEC_KEYS.to_string(),
286 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
287 read_only: Some(false), ..Default::default()
289 });
290
291 debug!(
292 secret_name = %secret.name,
293 "Mounting user-supplied DNSSEC keys from Secret"
294 );
295 }
296
297 None
300 | Some(crate::crd::DNSSECKeySource {
301 secret_ref: None,
302 persistent_volume: None,
303 }) => {
304 if signing_config.auto_generate.unwrap_or(true) {
305 volumes.push(Volume {
306 name: VOLUME_DNSSEC_KEYS.to_string(),
307 empty_dir: Some(EmptyDirVolumeSource::default()),
308 ..Default::default()
309 });
310
311 volume_mounts.push(VolumeMount {
312 name: VOLUME_DNSSEC_KEYS.to_string(),
313 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
314 ..Default::default()
315 });
316
317 debug!("DNSSEC keys will be auto-generated by BIND9 in emptyDir");
318
319 if signing_config.export_to_secret.unwrap_or(true) {
320 debug!("Auto-generated keys will be exported to Secret for backup/restore");
321 }
322 }
323 }
324
325 Some(crate::crd::DNSSECKeySource {
327 persistent_volume: Some(_pvc),
328 ..
329 }) => {
330 warn!("Persistent storage for DNSSEC keys is not yet implemented - using emptyDir");
331 volumes.push(Volume {
332 name: VOLUME_DNSSEC_KEYS.to_string(),
333 empty_dir: Some(EmptyDirVolumeSource::default()),
334 ..Default::default()
335 });
336
337 volume_mounts.push(VolumeMount {
338 name: VOLUME_DNSSEC_KEYS.to_string(),
339 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
340 ..Default::default()
341 });
342 }
343 }
344
345 (volumes, volume_mounts)
346}
347
348#[must_use]
374pub fn build_cluster_labels(cluster_name: &str) -> BTreeMap<String, String> {
375 let mut labels = BTreeMap::new();
376 labels.insert("app".into(), APP_NAME_BIND9.into());
377 labels.insert("cluster".into(), cluster_name.into());
378 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
379 labels.insert(K8S_INSTANCE.into(), cluster_name.into());
380 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_CLUSTER.into());
381 labels.insert(K8S_MANAGED_BY.into(), MANAGED_BY_BIND9_CLUSTER.into());
382 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
383 labels
384}
385
386#[must_use]
407pub fn build_labels_from_instance(
408 instance_name: &str,
409 instance: &Bind9Instance,
410) -> BTreeMap<String, String> {
411 use crate::labels::{BINDY_MANAGED_BY_LABEL, BINDY_ROLE_LABEL};
412
413 let mut labels = BTreeMap::new();
414 labels.insert("app".into(), APP_NAME_BIND9.into());
415 labels.insert("instance".into(), instance_name.into());
416 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
417 labels.insert(K8S_INSTANCE.into(), instance_name.into());
418 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
419 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
420
421 let managed_by = instance
424 .metadata
425 .labels
426 .as_ref()
427 .and_then(|labels| labels.get(BINDY_MANAGED_BY_LABEL))
428 .map_or(MANAGED_BY_BIND9_INSTANCE, String::as_str);
429
430 labels.insert(K8S_MANAGED_BY.into(), managed_by.into());
431
432 if let Some(instance_labels) = &instance.metadata.labels {
435 if let Some(role) = instance_labels.get(BINDY_ROLE_LABEL) {
436 labels.insert(BINDY_ROLE_LABEL.into(), role.clone());
437 }
438 }
439
440 labels
441}
442
443#[must_use]
456pub fn build_owner_references(instance: &Bind9Instance) -> Vec<OwnerReference> {
457 vec![OwnerReference {
458 api_version: API_GROUP_VERSION.to_string(),
459 kind: KIND_BIND9_INSTANCE.to_string(),
460 name: instance.name_any(),
461 uid: instance.metadata.uid.clone().unwrap_or_default(),
462 controller: Some(true),
463 block_owner_deletion: Some(true),
464 }]
465}
466
467pub fn build_configmap(
497 name: &str,
498 namespace: &str,
499 instance: &Bind9Instance,
500 cluster: Option<&Bind9Cluster>,
501 role_allow_transfer: Option<&Vec<String>>,
502) -> anyhow::Result<ConfigMap> {
503 debug!(
504 name = %name,
505 namespace = %namespace,
506 "Building ConfigMap for Bind9Instance"
507 );
508
509 let config_map_refs = instance
511 .spec
512 .config_map_refs
513 .as_ref()
514 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
515
516 let named_conf_overridden = config_map_refs.is_some_and(|refs| refs.named_conf.is_some());
517 let options_overridden = config_map_refs.is_some_and(|refs| refs.named_conf_options.is_some());
518
519 let mut data = BTreeMap::new();
521 let labels = build_labels_from_instance(name, instance);
522
523 if !named_conf_overridden {
525 let named_conf = build_named_conf(instance, cluster);
526 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
527 }
528
529 if !options_overridden {
532 let options_conf = build_options_conf(instance, cluster, role_allow_transfer)?;
533 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
534 }
535
536 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
539
540 let owner_refs = build_owner_references(instance);
544
545 Ok(ConfigMap {
546 metadata: ObjectMeta {
547 name: Some(format!("{name}-config")),
548 namespace: Some(namespace.into()),
549 labels: Some(labels),
550 owner_references: Some(owner_refs),
551 ..Default::default()
552 },
553 data: Some(data),
554 ..Default::default()
555 })
556}
557
558pub fn build_cluster_configmap(
578 cluster_name: &str,
579 namespace: &str,
580 cluster: &Bind9Cluster,
581) -> Result<ConfigMap, anyhow::Error> {
582 debug!(
583 cluster_name = %cluster_name,
584 namespace = %namespace,
585 "Building cluster-level shared ConfigMap"
586 );
587
588 let mut data = BTreeMap::new();
590 let labels = build_cluster_labels(cluster_name);
591
592 let named_conf = build_cluster_named_conf(cluster);
594 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
595
596 let options_conf = build_cluster_options_conf(cluster)?;
598 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
599
600 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
602
603 Ok(ConfigMap {
604 metadata: ObjectMeta {
605 name: Some(format!("{cluster_name}-config")),
606 namespace: Some(namespace.into()),
607 labels: Some(labels),
608 ..Default::default()
609 },
610 data: Some(data),
611 ..Default::default()
612 })
613}
614
615fn build_named_conf(instance: &Bind9Instance, cluster: Option<&Bind9Cluster>) -> String {
629 let config_map_refs = instance
631 .spec
632 .config_map_refs
633 .as_ref()
634 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
635
636 let zones_include = if let Some(refs) = config_map_refs {
637 if refs.named_conf_zones.is_some() {
638 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
640 } else {
641 String::new()
643 }
644 } else {
645 String::new()
647 };
648
649 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
653 let rndc_key_names = "\"bindy-operator\"";
654
655 NAMED_CONF_TEMPLATE
656 .replace("{{ZONES_INCLUDE}}", &zones_include)
657 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
658 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
659}
660
661#[allow(clippy::too_many_lines)]
683fn build_options_conf(
684 instance: &Bind9Instance,
685 cluster: Option<&Bind9Cluster>,
686 role_allow_transfer: Option<&Vec<String>>,
687) -> anyhow::Result<String> {
688 let recursion;
689 let mut allow_query = String::new();
690 let allow_transfer;
691 let mut dnssec_validate = String::new();
692
693 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
695
696 if let Some(config) = &instance.spec.config {
697 let recursion_value = if let Some(rec) = config.recursion {
699 if rec {
700 "yes"
701 } else {
702 "no"
703 }
704 } else if let Some(global) = global_config {
705 if global.recursion.unwrap_or(false) {
706 "yes"
707 } else {
708 "no"
709 }
710 } else {
711 "no"
712 };
713 recursion = format!("recursion {recursion_value};");
714
715 if let Some(acls) = &config.allow_query {
717 if !acls.is_empty() {
718 let acl_list = build_acl_list(acls)
719 .context("invalid entry in instance spec.config.allow_query")?;
720 allow_query = format!("allow-query {{ {acl_list}; }};");
721 }
722 } else if let Some(global) = global_config {
723 if let Some(global_acls) = &global.allow_query {
724 if !global_acls.is_empty() {
725 let acl_list = build_acl_list(global_acls)
726 .context("invalid entry in cluster spec.global.allow_query")?;
727 allow_query = format!("allow-query {{ {acl_list}; }};");
728 }
729 }
730 }
731
732 if let Some(acls) = &config.allow_transfer {
734 let acl_list = if acls.is_empty() {
736 "none".to_string()
737 } else {
738 build_acl_list(acls)
739 .context("invalid entry in instance spec.config.allow_transfer")?
740 };
741 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
742 } else if let Some(role_acls) = role_allow_transfer {
743 let acl_list = if role_acls.is_empty() {
745 "none".to_string()
746 } else {
747 build_acl_list(role_acls)
748 .context("invalid entry in cluster role-specific allow_transfer")?
749 };
750 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
751 } else if let Some(global) = global_config {
752 if let Some(global_acls) = &global.allow_transfer {
754 let acl_list = if global_acls.is_empty() {
755 "none".to_string()
756 } else {
757 build_acl_list(global_acls)
758 .context("invalid entry in cluster spec.global.allow_transfer")?
759 };
760 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
761 } else {
762 allow_transfer = String::new();
763 }
764 } else {
765 allow_transfer = String::new();
767 }
768
769 if let Some(dnssec) = &config.dnssec {
773 if dnssec.validation.unwrap_or(false) {
774 dnssec_validate = "dnssec-validation yes;".to_string();
775 } else {
776 dnssec_validate = "dnssec-validation no;".to_string();
777 }
778 } else if let Some(global) = global_config {
779 if let Some(global_dnssec) = &global.dnssec {
780 if global_dnssec.validation.unwrap_or(false) {
781 dnssec_validate = "dnssec-validation yes;".to_string();
782 } else {
783 dnssec_validate = "dnssec-validation no;".to_string();
784 }
785 }
786 }
787 } else {
788 if let Some(global) = global_config {
790 let recursion_value = if global.recursion.unwrap_or(false) {
792 "yes"
793 } else {
794 "no"
795 };
796 recursion = format!("recursion {recursion_value};");
797
798 if let Some(acls) = &global.allow_query {
800 if !acls.is_empty() {
801 let acl_list = build_acl_list(acls)
802 .context("invalid entry in cluster spec.global.allow_query")?;
803 allow_query = format!("allow-query {{ {acl_list}; }};");
804 }
805 }
806
807 if let Some(role_acls) = role_allow_transfer {
809 let acl_list = if role_acls.is_empty() {
810 "none".to_string()
811 } else {
812 build_acl_list(role_acls)
813 .context("invalid entry in cluster role-specific allow_transfer")?
814 };
815 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
816 } else if let Some(global_acls) = &global.allow_transfer {
817 let acl_list = if global_acls.is_empty() {
818 "none".to_string()
819 } else {
820 build_acl_list(global_acls)
821 .context("invalid entry in cluster spec.global.allow_transfer")?
822 };
823 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
824 } else {
825 allow_transfer = String::new();
826 }
827
828 if let Some(dnssec) = &global.dnssec {
830 if dnssec.validation.unwrap_or(false) {
831 dnssec_validate = "dnssec-validation yes;".to_string();
832 }
833 }
834 } else {
835 recursion = "recursion no;".to_string();
837 allow_transfer = String::new();
839 }
840 }
841
842 let dnssec_policies = generate_dnssec_policies(global_config, instance.spec.config.as_ref());
844
845 let instance_cfg = instance.spec.config.as_ref();
847 let forwarders = render_forwarders(
848 instance_cfg
849 .and_then(|c| c.forwarders.as_ref())
850 .or_else(|| global_config.and_then(|g| g.forwarders.as_ref())),
851 )?;
852 let listen_on = render_listen_on(
853 LISTEN_ON_DIRECTIVE,
854 instance_cfg
855 .and_then(|c| c.listen_on.as_ref())
856 .or_else(|| global_config.and_then(|g| g.listen_on.as_ref())),
857 )?;
858 let listen_on_v6 = render_listen_on(
859 LISTEN_ON_V6_DIRECTIVE,
860 instance_cfg
861 .and_then(|c| c.listen_on_v6.as_ref())
862 .or_else(|| global_config.and_then(|g| g.listen_on_v6.as_ref())),
863 )?;
864
865 Ok(NAMED_CONF_OPTIONS_TEMPLATE
867 .replace("{{LISTEN_ON}}", &listen_on)
868 .replace("{{LISTEN_ON_V6}}", &listen_on_v6)
869 .replace("{{RECURSION}}", &recursion)
870 .replace("{{FORWARDERS}}", &forwarders)
871 .replace("{{ALLOW_QUERY}}", &allow_query)
872 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
873 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
874 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies))
875}
876
877fn render_forwarders(forwarders: Option<&Vec<String>>) -> anyhow::Result<String> {
893 let Some(list) = forwarders else {
894 return Ok(String::new());
895 };
896 if list.is_empty() {
897 return Ok(String::new());
898 }
899
900 for entry in list {
901 let trimmed = entry.trim();
902 if trimmed.parse::<std::net::IpAddr>().is_err() {
903 anyhow::bail!("invalid forwarder {trimmed:?}: must be a plain IPv4 or IPv6 address");
904 }
905 }
906
907 let joined = list
908 .iter()
909 .map(|entry| entry.trim().to_string())
910 .collect::<Vec<_>>()
911 .join("; ");
912 Ok(format!("forwarders {{ {joined}; }};"))
913}
914
915fn render_listen_on(directive: &str, addresses: Option<&Vec<String>>) -> anyhow::Result<String> {
931 let list = match addresses {
932 Some(addrs) if !addrs.is_empty() => build_acl_list(addrs)
933 .with_context(|| format!("invalid entry in {directive} address list"))?,
934 _ => LISTEN_ON_DEFAULT.to_string(),
935 };
936 Ok(format!(
937 "{directive} port {DNS_CONTAINER_PORT} {{ {list}; }};"
938 ))
939}
940
941fn build_cluster_named_conf(cluster: &Bind9Cluster) -> String {
954 let zones_include = if let Some(refs) = &cluster.spec.common.config_map_refs {
956 if refs.named_conf_zones.is_some() {
957 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
959 } else {
960 String::new()
962 }
963 } else {
964 String::new()
966 };
967
968 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
972 let rndc_key_names = "\"bindy-operator\"";
973
974 NAMED_CONF_TEMPLATE
975 .replace("{{ZONES_INCLUDE}}", &zones_include)
976 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
977 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
978}
979
980#[allow(clippy::too_many_lines)]
994fn build_cluster_options_conf(cluster: &Bind9Cluster) -> anyhow::Result<String> {
995 let recursion;
996 let mut allow_query = String::new();
997 let mut allow_transfer = String::new();
998 let mut dnssec_validate = String::new();
999
1000 if let Some(global) = &cluster.spec.common.global {
1002 let recursion_value = if global.recursion.unwrap_or(false) {
1004 "yes"
1005 } else {
1006 "no"
1007 };
1008 recursion = format!("recursion {recursion_value};");
1009
1010 if let Some(aq) = &global.allow_query {
1012 if !aq.is_empty() {
1013 let acl_list = build_acl_list(aq)
1014 .context("invalid entry in cluster spec.global.allow_query")?;
1015 allow_query = format!("allow-query {{ {acl_list}; }};");
1016 }
1017 }
1018
1019 if let Some(at) = &global.allow_transfer {
1021 if !at.is_empty() {
1022 let acl_list = build_acl_list(at)
1023 .context("invalid entry in cluster spec.global.allow_transfer")?;
1024 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
1025 }
1026 }
1027
1028 if let Some(dnssec) = &global.dnssec {
1030 if dnssec.validation.unwrap_or(false) {
1031 dnssec_validate = "dnssec-validation yes;".to_string();
1032 } else {
1033 dnssec_validate = "dnssec-validation no;".to_string();
1034 }
1035 }
1036 } else {
1037 recursion = "recursion no;".to_string();
1039 }
1040
1041 let dnssec_policies = generate_dnssec_policies(cluster.spec.common.global.as_ref(), None);
1043
1044 let global = cluster.spec.common.global.as_ref();
1046 let forwarders = render_forwarders(global.and_then(|g| g.forwarders.as_ref()))?;
1047 let listen_on = render_listen_on(
1048 LISTEN_ON_DIRECTIVE,
1049 global.and_then(|g| g.listen_on.as_ref()),
1050 )?;
1051 let listen_on_v6 = render_listen_on(
1052 LISTEN_ON_V6_DIRECTIVE,
1053 global.and_then(|g| g.listen_on_v6.as_ref()),
1054 )?;
1055
1056 Ok(NAMED_CONF_OPTIONS_TEMPLATE
1057 .replace("{{LISTEN_ON}}", &listen_on)
1058 .replace("{{LISTEN_ON_V6}}", &listen_on_v6)
1059 .replace("{{RECURSION}}", &recursion)
1060 .replace("{{FORWARDERS}}", &forwarders)
1061 .replace("{{ALLOW_QUERY}}", &allow_query)
1062 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
1063 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
1064 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies))
1065}
1066
1067#[must_use]
1090struct DeploymentConfig<'a> {
1092 image_config: Option<&'a ImageConfig>,
1093 config_map_refs: Option<&'a ConfigMapRefs>,
1094 version: &'a str,
1095 volumes: Option<&'a Vec<Volume>>,
1096 volume_mounts: Option<&'a Vec<VolumeMount>>,
1097 bindcar_config: Option<&'a crate::crd::BindcarConfig>,
1098 configmap_name: String,
1099}
1100
1101fn resolve_deployment_config<'a>(
1103 name: &str,
1104 instance: &'a Bind9Instance,
1105 cluster: Option<&'a Bind9Cluster>,
1106 cluster_provider: Option<&'a crate::crd::ClusterBind9Provider>,
1107) -> DeploymentConfig<'a> {
1108 let image_config = instance
1110 .spec
1111 .image
1112 .as_ref()
1113 .or_else(|| cluster.and_then(|c| c.spec.common.image.as_ref()))
1114 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.image.as_ref()));
1115
1116 let config_map_refs = instance
1118 .spec
1119 .config_map_refs
1120 .as_ref()
1121 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()))
1122 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.config_map_refs.as_ref()));
1123
1124 let version = instance
1126 .spec
1127 .version
1128 .as_deref()
1129 .or_else(|| cluster.and_then(|c| c.spec.common.version.as_deref()))
1130 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.version.as_deref()))
1131 .unwrap_or(DEFAULT_BIND9_VERSION);
1132
1133 let volumes = instance
1135 .spec
1136 .volumes
1137 .as_ref()
1138 .or_else(|| cluster.and_then(|c| c.spec.common.volumes.as_ref()))
1139 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volumes.as_ref()));
1140
1141 let volume_mounts = instance
1143 .spec
1144 .volume_mounts
1145 .as_ref()
1146 .or_else(|| cluster.and_then(|c| c.spec.common.volume_mounts.as_ref()))
1147 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volume_mounts.as_ref()));
1148
1149 let bindcar_config = instance
1151 .spec
1152 .bindcar_config
1153 .as_ref()
1154 .or_else(|| {
1155 cluster.and_then(|c| {
1156 c.spec
1157 .common
1158 .global
1159 .as_ref()
1160 .and_then(|g| g.bindcar_config.as_ref())
1161 })
1162 })
1163 .or_else(|| {
1164 cluster_provider.and_then(|cp| {
1165 cp.spec
1166 .common
1167 .global
1168 .as_ref()
1169 .and_then(|g| g.bindcar_config.as_ref())
1170 })
1171 });
1172
1173 let configmap_name = if instance.spec.cluster_ref.is_empty() {
1175 format!("{name}-config")
1177 } else {
1178 format!("{}-config", instance.spec.cluster_ref)
1180 };
1181
1182 DeploymentConfig {
1183 image_config,
1184 config_map_refs,
1185 version,
1186 volumes,
1187 volume_mounts,
1188 bindcar_config,
1189 configmap_name,
1190 }
1191}
1192
1193pub fn build_deployment(
1194 name: &str,
1195 namespace: &str,
1196 instance: &Bind9Instance,
1197 cluster: Option<&Bind9Cluster>,
1198 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1199 rndc_secret_name: &str,
1200) -> Deployment {
1201 debug!(
1202 name = %name,
1203 namespace = %namespace,
1204 has_cluster = cluster.is_some(),
1205 has_cluster_provider = cluster_provider.is_some(),
1206 "Building Deployment for Bind9Instance"
1207 );
1208
1209 let labels = build_labels_from_instance(name, instance);
1211 let replicas = instance.spec.replicas.unwrap_or(1);
1212 debug!(replicas, "Deployment replica count");
1213
1214 let config = resolve_deployment_config(name, instance, cluster, cluster_provider);
1215
1216 let owner_refs = build_owner_references(instance);
1217
1218 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
1220 let instance_config = instance.spec.config.as_ref();
1221
1222 let (dnssec_volumes, dnssec_volume_mounts) =
1224 build_dnssec_key_volumes(global_config, instance_config);
1225
1226 let all_volumes = if dnssec_volumes.is_empty() {
1228 config.volumes.map(std::borrow::ToOwned::to_owned)
1229 } else {
1230 let mut merged = dnssec_volumes;
1231 if let Some(custom) = config.volumes {
1232 merged.extend(custom.iter().cloned());
1233 }
1234 Some(merged)
1235 };
1236
1237 let all_volume_mounts = if dnssec_volume_mounts.is_empty() {
1239 config.volume_mounts.map(std::borrow::ToOwned::to_owned)
1240 } else {
1241 let mut merged = dnssec_volume_mounts;
1242 if let Some(custom) = config.volume_mounts {
1243 merged.extend(custom.iter().cloned());
1244 }
1245 Some(merged)
1246 };
1247
1248 Deployment {
1249 metadata: ObjectMeta {
1250 name: Some(name.into()),
1251 namespace: Some(namespace.into()),
1252 labels: Some(labels.clone()),
1253 owner_references: Some(owner_refs),
1254 ..Default::default()
1255 },
1256 spec: Some(DeploymentSpec {
1257 replicas: Some(replicas),
1258 selector: LabelSelector {
1259 match_labels: Some(labels.clone()),
1260 ..Default::default()
1261 },
1262 template: PodTemplateSpec {
1263 metadata: Some(ObjectMeta {
1264 labels: Some(labels.clone()),
1265 ..Default::default()
1266 }),
1267 spec: Some(build_pod_spec(
1268 &config.configmap_name,
1269 rndc_secret_name,
1270 config.version,
1271 config.image_config,
1272 config.config_map_refs,
1273 all_volumes.as_ref(),
1274 all_volume_mounts.as_ref(),
1275 config.bindcar_config,
1276 )),
1277 },
1278 ..Default::default()
1279 }),
1280 ..Default::default()
1281 }
1282}
1283
1284#[allow(clippy::too_many_arguments)]
1296#[allow(clippy::too_many_lines)]
1297fn build_pod_spec(
1298 configmap_name: &str,
1299 rndc_secret_name: &str,
1300 version: &str,
1301 image_config: Option<&ImageConfig>,
1302 config_map_refs: Option<&ConfigMapRefs>,
1303 custom_volumes: Option<&Vec<Volume>>,
1304 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1305 bindcar_config: Option<&crate::crd::BindcarConfig>,
1306) -> PodSpec {
1307 let image = if let Some(img_cfg) = image_config {
1309 img_cfg
1310 .image
1311 .clone()
1312 .unwrap_or_else(|| format!("internetsystemsconsortium/bind9:{version}"))
1313 } else {
1314 format!("internetsystemsconsortium/bind9:{version}")
1315 };
1316
1317 let image_pull_policy = image_config
1319 .and_then(|cfg| cfg.image_pull_policy.clone())
1320 .unwrap_or_else(|| "IfNotPresent".into());
1321
1322 let bind9_container = Container {
1324 name: CONTAINER_NAME_BIND9.into(),
1325 image: Some(image),
1326 image_pull_policy: Some(image_pull_policy),
1327 command: Some(vec!["named".into()]),
1328 args: Some(vec![
1329 "-c".into(),
1330 BIND_NAMED_CONF_PATH.into(),
1331 "-g".into(), ]),
1333 ports: Some(vec![
1334 ContainerPort {
1335 name: Some("dns-tcp".into()),
1336 container_port: i32::from(DNS_CONTAINER_PORT),
1337 protocol: Some("TCP".into()),
1338 ..Default::default()
1339 },
1340 ContainerPort {
1341 name: Some("dns-udp".into()),
1342 container_port: i32::from(DNS_CONTAINER_PORT),
1343 protocol: Some("UDP".into()),
1344 ..Default::default()
1345 },
1346 ContainerPort {
1347 name: Some("rndc".into()),
1348 container_port: i32::from(RNDC_PORT),
1349 protocol: Some("TCP".into()),
1350 ..Default::default()
1351 },
1352 ]),
1353 env: Some(vec![
1354 EnvVar {
1355 name: "TZ".into(),
1356 value: Some("UTC".into()),
1357 ..Default::default()
1358 },
1359 EnvVar {
1360 name: "MALLOC_CONF".into(),
1361 value: Some(BIND9_MALLOC_CONF.into()),
1362 ..Default::default()
1363 },
1364 ]),
1365 volume_mounts: Some(build_volume_mounts(config_map_refs, custom_volume_mounts)),
1366 liveness_probe: Some(Probe {
1367 tcp_socket: Some(TCPSocketAction {
1368 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1369 ..Default::default()
1370 }),
1371 initial_delay_seconds: Some(LIVENESS_INITIAL_DELAY_SECS),
1372 period_seconds: Some(LIVENESS_PERIOD_SECS),
1373 timeout_seconds: Some(LIVENESS_TIMEOUT_SECS),
1374 failure_threshold: Some(LIVENESS_FAILURE_THRESHOLD),
1375 ..Default::default()
1376 }),
1377 readiness_probe: Some(Probe {
1378 tcp_socket: Some(TCPSocketAction {
1379 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1380 ..Default::default()
1381 }),
1382 initial_delay_seconds: Some(READINESS_INITIAL_DELAY_SECS),
1383 period_seconds: Some(READINESS_PERIOD_SECS),
1384 timeout_seconds: Some(READINESS_TIMEOUT_SECS),
1385 failure_threshold: Some(READINESS_FAILURE_THRESHOLD),
1386 ..Default::default()
1387 }),
1388 security_context: Some(SecurityContext {
1389 run_as_non_root: Some(true),
1390 run_as_user: Some(BIND9_NONROOT_UID),
1391 run_as_group: Some(BIND9_NONROOT_UID),
1392 allow_privilege_escalation: Some(false),
1393 capabilities: Some(Capabilities {
1394 drop: Some(vec!["ALL".to_string()]),
1399 add: None,
1400 }),
1401 seccomp_profile: Some(SeccompProfile {
1405 type_: "RuntimeDefault".to_string(),
1406 ..Default::default()
1407 }),
1408 ..Default::default()
1409 }),
1410 ..Default::default()
1411 };
1412
1413 let image_pull_secrets = image_config.and_then(|cfg| {
1415 cfg.image_pull_secrets.as_ref().map(|secrets| {
1416 secrets
1417 .iter()
1418 .map(|s| k8s_openapi::api::core::v1::LocalObjectReference { name: s.clone() })
1419 .collect()
1420 })
1421 });
1422
1423 PodSpec {
1424 containers: {
1425 let mut containers = vec![bind9_container];
1426 containers.push(build_api_sidecar_container(
1427 bindcar_config,
1428 rndc_secret_name,
1429 ));
1430 containers
1431 },
1432 volumes: Some(build_volumes(
1433 configmap_name,
1434 rndc_secret_name,
1435 config_map_refs,
1436 custom_volumes,
1437 )),
1438 image_pull_secrets,
1439 service_account_name: Some(BIND9_SERVICE_ACCOUNT.into()),
1440 security_context: Some(PodSecurityContext {
1441 run_as_user: Some(BIND9_NONROOT_UID),
1442 run_as_group: Some(BIND9_NONROOT_UID),
1443 fs_group: Some(BIND9_NONROOT_UID),
1444 run_as_non_root: Some(true),
1445 seccomp_profile: Some(SeccompProfile {
1449 type_: "RuntimeDefault".to_string(),
1450 ..Default::default()
1451 }),
1452 ..Default::default()
1453 }),
1454 ..Default::default()
1455 }
1456}
1457
1458#[allow(clippy::too_many_lines)]
1469fn build_api_sidecar_container(
1470 bindcar_config: Option<&crate::crd::BindcarConfig>,
1471 rndc_secret_name: &str,
1472) -> Container {
1473 let image = bindcar_config
1475 .and_then(|c| c.image.clone())
1476 .unwrap_or_else(|| crate::constants::DEFAULT_BINDCAR_IMAGE.to_string());
1477
1478 let image_pull_policy = bindcar_config
1479 .and_then(|c| c.image_pull_policy.clone())
1480 .unwrap_or_else(|| "IfNotPresent".to_string());
1481
1482 let port = bindcar_config
1483 .and_then(|c| c.port)
1484 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
1485
1486 let log_level = bindcar_config
1487 .and_then(|c| c.log_level.clone())
1488 .unwrap_or_else(|| "info".to_string());
1489
1490 let resources = bindcar_config.and_then(|c| c.resources.clone());
1491
1492 let operator_namespace = std::env::var("POD_NAMESPACE")
1498 .unwrap_or_else(|_| crate::constants::DEFAULT_OPERATOR_NAMESPACE.to_string());
1499 let allowed_service_account = format!(
1500 "system:serviceaccount:{operator_namespace}:{}",
1501 crate::constants::OPERATOR_SERVICE_ACCOUNT
1502 );
1503
1504 let mut env_vars = vec![
1506 EnvVar {
1507 name: "BIND_ZONE_DIR".into(),
1508 value: Some(BIND_CACHE_PATH.into()),
1509 ..Default::default()
1510 },
1511 EnvVar {
1512 name: "API_PORT".into(),
1513 value: Some(port.to_string()),
1514 ..Default::default()
1515 },
1516 EnvVar {
1517 name: "RUST_LOG".into(),
1518 value: Some(log_level),
1519 ..Default::default()
1520 },
1521 EnvVar {
1522 name: "BIND_ALLOWED_SERVICE_ACCOUNTS".into(),
1523 value: Some(allowed_service_account),
1524 ..Default::default()
1525 },
1526 EnvVar {
1530 name: "BIND_TOKEN_AUDIENCES".into(),
1531 value: Some(crate::constants::BINDCAR_TOKEN_AUDIENCE.into()),
1532 ..Default::default()
1533 },
1534 EnvVar {
1537 name: "TMPDIR".into(),
1538 value: Some(crate::constants::BINDCAR_TMP_PATH.into()),
1539 ..Default::default()
1540 },
1541 EnvVar {
1542 name: "RNDC_SECRET".into(),
1543 value_from: Some(EnvVarSource {
1544 secret_key_ref: Some(SecretKeySelector {
1545 name: rndc_secret_name.to_string(),
1546 key: "secret".to_string(),
1547 optional: Some(false),
1548 }),
1549 ..Default::default()
1550 }),
1551 ..Default::default()
1552 },
1553 EnvVar {
1554 name: "RNDC_ALGORITHM".into(),
1555 value_from: Some(EnvVarSource {
1556 secret_key_ref: Some(SecretKeySelector {
1557 name: rndc_secret_name.to_string(),
1558 key: "algorithm".to_string(),
1559 optional: Some(false),
1560 }),
1561 ..Default::default()
1562 }),
1563 ..Default::default()
1564 },
1565 EnvVar {
1570 name: "NSUPDATE_PORT".into(),
1571 value: Some(DNS_CONTAINER_PORT.to_string()),
1572 ..Default::default()
1573 },
1574 ];
1575
1576 if let Some(config) = bindcar_config {
1578 if let Some(user_env_vars) = &config.env_vars {
1579 env_vars.extend(user_env_vars.clone());
1580 }
1581 }
1582
1583 Container {
1584 name: CONTAINER_NAME_BINDCAR.into(),
1585 image: Some(image),
1586 image_pull_policy: Some(image_pull_policy),
1587 ports: Some(vec![ContainerPort {
1588 name: Some("http".into()),
1589 container_port: port,
1590 protocol: Some("TCP".into()),
1591 ..Default::default()
1592 }]),
1593 env: Some(env_vars),
1594 volume_mounts: Some(vec![
1595 VolumeMount {
1596 name: VOLUME_CACHE.into(),
1597 mount_path: BIND_CACHE_PATH.into(),
1598 ..Default::default()
1599 },
1600 VolumeMount {
1601 name: VOLUME_RNDC_KEY.into(),
1602 mount_path: BIND_KEYS_PATH.into(),
1603 read_only: Some(true),
1604 ..Default::default()
1605 },
1606 VolumeMount {
1607 name: VOLUME_CONFIG.into(),
1608 mount_path: BIND_RNDC_CONF_PATH.into(),
1609 sub_path: Some(RNDC_CONF_FILENAME.into()),
1610 ..Default::default()
1611 },
1612 VolumeMount {
1615 name: VOLUME_TMP.into(),
1616 mount_path: crate::constants::BINDCAR_TMP_PATH.into(),
1617 ..Default::default()
1618 },
1619 ]),
1620 resources,
1621 security_context: Some(SecurityContext {
1622 run_as_non_root: Some(true),
1623 run_as_user: Some(BIND9_NONROOT_UID),
1624 run_as_group: Some(BIND9_NONROOT_UID),
1625 allow_privilege_escalation: Some(false),
1626 read_only_root_filesystem: Some(true),
1630 capabilities: Some(Capabilities {
1631 drop: Some(vec!["ALL".to_string()]),
1632 ..Default::default()
1633 }),
1634 seccomp_profile: Some(SeccompProfile {
1635 type_: "RuntimeDefault".to_string(),
1636 ..Default::default()
1637 }),
1638 ..Default::default()
1639 }),
1640 ..Default::default()
1641 }
1642}
1643
1644fn build_volume_mounts(
1662 config_map_refs: Option<&ConfigMapRefs>,
1663 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1664) -> Vec<VolumeMount> {
1665 let mut mounts = vec![
1666 VolumeMount {
1667 name: VOLUME_ZONES.into(),
1668 mount_path: BIND_ZONES_PATH.into(),
1669 ..Default::default()
1670 },
1671 VolumeMount {
1672 name: VOLUME_CACHE.into(),
1673 mount_path: BIND_CACHE_PATH.into(),
1674 ..Default::default()
1675 },
1676 VolumeMount {
1677 name: VOLUME_RNDC_KEY.into(),
1678 mount_path: BIND_KEYS_PATH.into(),
1679 read_only: Some(true),
1680 ..Default::default()
1681 },
1682 ];
1683
1684 if let Some(refs) = config_map_refs {
1686 if let Some(_configmap_name) = &refs.named_conf {
1687 mounts.push(VolumeMount {
1688 name: VOLUME_NAMED_CONF.into(),
1689 mount_path: BIND_NAMED_CONF_PATH.into(),
1690 sub_path: Some(NAMED_CONF_FILENAME.into()),
1691 ..Default::default()
1692 });
1693 } else {
1694 mounts.push(VolumeMount {
1696 name: VOLUME_CONFIG.into(),
1697 mount_path: BIND_NAMED_CONF_PATH.into(),
1698 sub_path: Some(NAMED_CONF_FILENAME.into()),
1699 ..Default::default()
1700 });
1701 }
1702
1703 if let Some(_configmap_name) = &refs.named_conf_options {
1704 mounts.push(VolumeMount {
1705 name: VOLUME_NAMED_CONF_OPTIONS.into(),
1706 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1707 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1708 ..Default::default()
1709 });
1710 } else {
1711 mounts.push(VolumeMount {
1713 name: VOLUME_CONFIG.into(),
1714 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1715 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1716 ..Default::default()
1717 });
1718 }
1719
1720 if let Some(_configmap_name) = &refs.named_conf_zones {
1722 mounts.push(VolumeMount {
1723 name: VOLUME_NAMED_CONF_ZONES.into(),
1724 mount_path: BIND_NAMED_CONF_ZONES_PATH.into(),
1725 sub_path: Some(NAMED_CONF_ZONES_FILENAME.into()),
1726 ..Default::default()
1727 });
1728 }
1729 } else {
1731 mounts.push(VolumeMount {
1733 name: VOLUME_CONFIG.into(),
1734 mount_path: BIND_NAMED_CONF_PATH.into(),
1735 sub_path: Some(NAMED_CONF_FILENAME.into()),
1736 ..Default::default()
1737 });
1738 mounts.push(VolumeMount {
1739 name: VOLUME_CONFIG.into(),
1740 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1741 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1742 ..Default::default()
1743 });
1744 }
1746
1747 mounts.push(VolumeMount {
1749 name: VOLUME_CONFIG.into(),
1750 mount_path: BIND_RNDC_CONF_PATH.into(),
1751 sub_path: Some(RNDC_CONF_FILENAME.into()),
1752 ..Default::default()
1753 });
1754
1755 if let Some(custom_mounts) = custom_volume_mounts {
1757 mounts.extend(custom_mounts.iter().cloned());
1758 }
1759
1760 mounts
1761}
1762
1763fn build_volumes(
1788 configmap_name: &str,
1789 rndc_secret_name: &str,
1790 config_map_refs: Option<&ConfigMapRefs>,
1791 custom_volumes: Option<&Vec<Volume>>,
1792) -> Vec<Volume> {
1793 let mut volumes = vec![
1794 Volume {
1795 name: VOLUME_ZONES.into(),
1796 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
1797 ..Default::default()
1798 },
1799 Volume {
1800 name: VOLUME_CACHE.into(),
1801 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
1802 ..Default::default()
1803 },
1804 Volume {
1805 name: VOLUME_RNDC_KEY.into(),
1806 secret: Some(k8s_openapi::api::core::v1::SecretVolumeSource {
1807 secret_name: Some(rndc_secret_name.to_string()),
1808 ..Default::default()
1809 }),
1810 ..Default::default()
1811 },
1812 Volume {
1816 name: VOLUME_TMP.into(),
1817 empty_dir: Some(EmptyDirVolumeSource {
1818 medium: Some("Memory".to_string()),
1819 ..Default::default()
1820 }),
1821 ..Default::default()
1822 },
1823 ];
1824
1825 if let Some(refs) = config_map_refs {
1827 if let Some(configmap_name) = &refs.named_conf {
1828 volumes.push(Volume {
1829 name: VOLUME_NAMED_CONF.into(),
1830 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1831 name: configmap_name.clone(),
1832 ..Default::default()
1833 }),
1834 ..Default::default()
1835 });
1836 }
1837
1838 if let Some(configmap_name) = &refs.named_conf_options {
1839 volumes.push(Volume {
1840 name: VOLUME_NAMED_CONF_OPTIONS.into(),
1841 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1842 name: configmap_name.clone(),
1843 ..Default::default()
1844 }),
1845 ..Default::default()
1846 });
1847 }
1848
1849 if let Some(configmap_name) = &refs.named_conf_zones {
1850 volumes.push(Volume {
1851 name: VOLUME_NAMED_CONF_ZONES.into(),
1852 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1853 name: configmap_name.clone(),
1854 ..Default::default()
1855 }),
1856 ..Default::default()
1857 });
1858 }
1859 }
1860
1861 volumes.push(Volume {
1867 name: VOLUME_CONFIG.into(),
1868 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
1869 name: configmap_name.to_string(),
1870 ..Default::default()
1871 }),
1872 ..Default::default()
1873 });
1874
1875 if let Some(custom_vols) = custom_volumes {
1877 volumes.extend(custom_vols.iter().cloned());
1878 }
1879
1880 volumes
1881}
1882
1883#[must_use]
1925pub fn build_service(
1926 name: &str,
1927 namespace: &str,
1928 instance: &Bind9Instance,
1929 custom_config: Option<&crate::crd::ServiceConfig>,
1930) -> Service {
1931 let labels = build_labels_from_instance(name, instance);
1933 let owner_refs = build_owner_references(instance);
1934
1935 let api_container_port = instance
1937 .spec
1938 .bindcar_config
1939 .as_ref()
1940 .and_then(|c| c.port)
1941 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
1942
1943 let mut default_spec = ServiceSpec {
1945 selector: Some(labels.clone()),
1946 ports: Some(vec![
1947 ServicePort {
1948 name: Some("dns-tcp".into()),
1949 port: i32::from(DNS_PORT),
1950 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
1951 protocol: Some("TCP".into()),
1952 ..Default::default()
1953 },
1954 ServicePort {
1955 name: Some("dns-udp".into()),
1956 port: i32::from(DNS_PORT),
1957 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
1958 protocol: Some("UDP".into()),
1959 ..Default::default()
1960 },
1961 ServicePort {
1962 name: Some("http".into()),
1963 port: i32::from(crate::constants::BINDCAR_SERVICE_PORT),
1964 target_port: Some(IntOrString::Int(api_container_port)),
1965 protocol: Some("TCP".into()),
1966 ..Default::default()
1967 },
1968 ]),
1969 type_: Some("ClusterIP".into()),
1970 ..Default::default()
1971 };
1972
1973 if let Some(bindcar_service_spec) = instance
1975 .spec
1976 .bindcar_config
1977 .as_ref()
1978 .and_then(|c| c.service_spec.as_ref())
1979 {
1980 merge_service_spec(&mut default_spec, bindcar_service_spec);
1981 }
1982
1983 let (custom_spec, custom_annotations) = custom_config.map_or((None, None), |config| {
1985 (config.spec.as_ref(), config.annotations.as_ref())
1986 });
1987
1988 if let Some(custom) = custom_spec {
1990 merge_service_spec(&mut default_spec, custom);
1991 }
1992
1993 let mut metadata = ObjectMeta {
1995 name: Some(name.into()),
1996 namespace: Some(namespace.into()),
1997 labels: Some(labels),
1998 owner_references: Some(owner_refs),
1999 ..Default::default()
2000 };
2001
2002 if let Some(annotations) = custom_annotations {
2004 metadata.annotations = Some(annotations.clone());
2005 }
2006
2007 Service {
2008 metadata,
2009 spec: Some(default_spec),
2010 ..Default::default()
2011 }
2012}
2013
2014#[must_use]
2041pub fn build_service_account(namespace: &str, _instance: &Bind9Instance) -> ServiceAccount {
2042 let mut labels = BTreeMap::new();
2049 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
2050 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
2051 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
2052
2053 ServiceAccount {
2054 metadata: ObjectMeta {
2055 name: Some(BIND9_SERVICE_ACCOUNT.into()),
2056 namespace: Some(namespace.into()),
2057 labels: Some(labels),
2058 owner_references: None, ..Default::default()
2060 },
2061 ..Default::default()
2062 }
2063}
2064
2065fn merge_service_spec(default: &mut ServiceSpec, custom: &ServiceSpec) {
2073 if let Some(ref type_) = custom.type_ {
2075 default.type_ = Some(type_.clone());
2076 }
2077
2078 if let Some(ref lb_ip) = custom.load_balancer_ip {
2080 default.load_balancer_ip = Some(lb_ip.clone());
2081 }
2082
2083 if let Some(ref affinity) = custom.session_affinity {
2085 default.session_affinity = Some(affinity.clone());
2086 }
2087
2088 if let Some(ref config) = custom.session_affinity_config {
2090 default.session_affinity_config = Some(config.clone());
2091 }
2092
2093 if let Some(ref cluster_ip) = custom.cluster_ip {
2095 default.cluster_ip = Some(cluster_ip.clone());
2096 }
2097
2098 if let Some(ref policy) = custom.external_traffic_policy {
2100 default.external_traffic_policy = Some(policy.clone());
2101 }
2102
2103 if let Some(ref ranges) = custom.load_balancer_source_ranges {
2105 default.load_balancer_source_ranges = Some(ranges.clone());
2106 }
2107
2108 if let Some(ref ips) = custom.external_ips {
2110 default.external_ips = Some(ips.clone());
2111 }
2112
2113 if let Some(ref class) = custom.load_balancer_class {
2115 default.load_balancer_class = Some(class.clone());
2116 }
2117
2118 if let Some(port) = custom.health_check_node_port {
2120 default.health_check_node_port = Some(port);
2121 }
2122
2123 if let Some(publish) = custom.publish_not_ready_addresses {
2125 default.publish_not_ready_addresses = Some(publish);
2126 }
2127
2128 if let Some(allocate) = custom.allocate_load_balancer_node_ports {
2130 default.allocate_load_balancer_node_ports = Some(allocate);
2131 }
2132
2133 if let Some(ref policy) = custom.internal_traffic_policy {
2135 default.internal_traffic_policy = Some(policy.clone());
2136 }
2137
2138 if let Some(ref families) = custom.ip_families {
2140 default.ip_families = Some(families.clone());
2141 }
2142
2143 if let Some(ref policy) = custom.ip_family_policy {
2145 default.ip_family_policy = Some(policy.clone());
2146 }
2147
2148 if let Some(ref ips) = custom.cluster_ips {
2150 default.cluster_ips = Some(ips.clone());
2151 }
2152
2153 if let Some(ref custom_ports) = custom.ports {
2155 if let Some(ref mut default_ports) = default.ports {
2156 for custom_port in custom_ports {
2158 if let Some(existing_port) = default_ports
2159 .iter_mut()
2160 .find(|p| p.name == custom_port.name)
2161 {
2162 *existing_port = custom_port.clone();
2164 } else {
2165 default_ports.push(custom_port.clone());
2167 }
2168 }
2169 } else {
2170 default.ports = Some(custom_ports.clone());
2172 }
2173 }
2174
2175 }