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
115const MAX_DNSSEC_POLICY_NAME_LEN: usize = 63;
118
119const MAX_DNSSEC_TOKEN_LEN: usize = 32;
122
123fn validate_dnssec_policy_name(name: &str) -> anyhow::Result<()> {
135 if name.is_empty() || name.len() > MAX_DNSSEC_POLICY_NAME_LEN {
136 anyhow::bail!(
137 "invalid dnssec policy name {name:?}: must be 1-{MAX_DNSSEC_POLICY_NAME_LEN} characters"
138 );
139 }
140
141 let mut chars = name.chars();
142 let first = chars.next().unwrap_or_default();
144 if !first.is_ascii_alphanumeric() {
145 anyhow::bail!(
146 "invalid dnssec policy name {name:?}: must start with an ASCII letter or digit"
147 );
148 }
149 if let Some(bad) = chars.find(|c| !c.is_ascii_alphanumeric() && *c != '-' && *c != '_') {
150 anyhow::bail!(
151 "invalid dnssec policy name {name:?}: illegal character {bad:?} \
152 (allowed: ASCII letters, digits, '-', '_')"
153 );
154 }
155
156 Ok(())
157}
158
159fn validate_dnssec_token(field: &str, value: &str) -> anyhow::Result<()> {
169 if value.is_empty() || value.len() > MAX_DNSSEC_TOKEN_LEN {
170 anyhow::bail!(
171 "invalid {field} {value:?}: must be 1-{MAX_DNSSEC_TOKEN_LEN} alphanumeric characters"
172 );
173 }
174 if let Some(bad) = value.chars().find(|c| !c.is_ascii_alphanumeric()) {
175 anyhow::bail!(
176 "invalid {field} {value:?}: illegal character {bad:?} (allowed: ASCII letters and digits)"
177 );
178 }
179
180 Ok(())
181}
182
183pub(crate) fn generate_dnssec_policies(
205 global_config: Option<&crate::crd::Bind9Config>,
206 instance_config: Option<&crate::crd::Bind9Config>,
207) -> anyhow::Result<String> {
208 let Some(signing) = get_dnssec_signing_config(global_config, instance_config) else {
213 return Ok(String::new());
214 };
215
216 let policy_name = signing
218 .policy
219 .as_deref()
220 .unwrap_or(DEFAULT_DNSSEC_POLICY_NAME);
221 let algorithm = signing
222 .algorithm
223 .as_deref()
224 .unwrap_or(DEFAULT_DNSSEC_ALGORITHM);
225 let ksk_lifetime = signing
226 .ksk_lifetime
227 .as_deref()
228 .unwrap_or(DEFAULT_KSK_LIFETIME);
229 let zsk_lifetime = signing
230 .zsk_lifetime
231 .as_deref()
232 .unwrap_or(DEFAULT_ZSK_LIFETIME);
233
234 let nsec_config = if signing.nsec3.unwrap_or(false) {
238 let iterations = signing.nsec3_iterations.unwrap_or(0);
239 let salt_length = DEFAULT_NSEC3_SALT_LENGTH;
240 format!(
241 "\n // Authenticated denial of existence (NSEC3)\n nsec3param iterations {iterations} optout no salt-length {salt_length};\n"
242 )
243 } else {
244 String::new()
245 };
246
247 validate_dnssec_policy_name(policy_name)?;
250 validate_dnssec_token("dnssec algorithm", algorithm)?;
251 validate_dnssec_token("dnssec ksk lifetime", ksk_lifetime)?;
252 validate_dnssec_token("dnssec zsk lifetime", zsk_lifetime)?;
253
254 Ok(DNSSEC_POLICY_TEMPLATE
256 .replace("{{POLICY_NAME}}", policy_name)
257 .replace("{{ALGORITHM}}", algorithm)
258 .replace("{{KSK_LIFETIME}}", ksk_lifetime)
259 .replace("{{ZSK_LIFETIME}}", zsk_lifetime)
260 .replace("{{NSEC_CONFIG}}", &nsec_config))
261}
262
263#[allow(dead_code)]
276pub(crate) fn is_dnssec_signing_enabled(
277 global_config: Option<&crate::crd::Bind9Config>,
278 instance_config: Option<&crate::crd::Bind9Config>,
279) -> bool {
280 let dnssec_config = if let Some(instance) = instance_config {
282 instance.dnssec.as_ref().and_then(|d| d.signing.as_ref())
283 } else {
284 global_config.and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
285 };
286
287 dnssec_config.is_some_and(|signing| signing.enabled)
288}
289
290pub(crate) fn get_dnssec_signing_config<'a>(
303 global_config: Option<&'a crate::crd::Bind9Config>,
304 instance_config: Option<&'a crate::crd::Bind9Config>,
305) -> Option<&'a crate::crd::DNSSECSigningConfig> {
306 if let Some(instance) = instance_config {
308 if let Some(config) = instance.dnssec.as_ref().and_then(|d| d.signing.as_ref()) {
309 if config.enabled {
310 return Some(config);
311 }
312 }
313 }
314
315 global_config
316 .and_then(|g| g.dnssec.as_ref().and_then(|d| d.signing.as_ref()))
317 .filter(|config| config.enabled)
318}
319
320pub(crate) fn build_dnssec_key_volumes(
336 global_config: Option<&crate::crd::Bind9Config>,
337 instance_config: Option<&crate::crd::Bind9Config>,
338) -> (Vec<Volume>, Vec<VolumeMount>) {
339 use k8s_openapi::api::core::v1::{
340 EmptyDirVolumeSource, SecretVolumeSource, Volume, VolumeMount,
341 };
342
343 let Some(signing_config) = get_dnssec_signing_config(global_config, instance_config) else {
344 return (vec![], vec![]);
345 };
346
347 let mut volumes = Vec::new();
348 let mut volume_mounts = Vec::new();
349
350 match &signing_config.keys_from {
352 Some(crate::crd::DNSSECKeySource {
354 secret_ref: Some(secret),
355 ..
356 }) => {
357 volumes.push(Volume {
358 name: VOLUME_DNSSEC_KEYS.to_string(),
359 secret: Some(SecretVolumeSource {
360 secret_name: Some(secret.name.clone()),
361 default_mode: Some(0o600), ..Default::default()
363 }),
364 ..Default::default()
365 });
366
367 volume_mounts.push(VolumeMount {
368 name: VOLUME_DNSSEC_KEYS.to_string(),
369 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
370 read_only: Some(false), ..Default::default()
372 });
373
374 debug!(
375 secret_name = %secret.name,
376 "Mounting user-supplied DNSSEC keys from Secret"
377 );
378 }
379
380 None
383 | Some(crate::crd::DNSSECKeySource {
384 secret_ref: None,
385 persistent_volume: None,
386 }) => {
387 if signing_config.auto_generate.unwrap_or(true) {
388 volumes.push(Volume {
389 name: VOLUME_DNSSEC_KEYS.to_string(),
390 empty_dir: Some(EmptyDirVolumeSource::default()),
391 ..Default::default()
392 });
393
394 volume_mounts.push(VolumeMount {
395 name: VOLUME_DNSSEC_KEYS.to_string(),
396 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
397 ..Default::default()
398 });
399
400 debug!("DNSSEC keys will be auto-generated by BIND9 in emptyDir");
401
402 if signing_config.export_to_secret.unwrap_or(true) {
403 debug!("Auto-generated keys will be exported to Secret for backup/restore");
404 }
405 }
406 }
407
408 Some(crate::crd::DNSSECKeySource {
410 persistent_volume: Some(_pvc),
411 ..
412 }) => {
413 warn!("Persistent storage for DNSSEC keys is not yet implemented - using emptyDir");
414 volumes.push(Volume {
415 name: VOLUME_DNSSEC_KEYS.to_string(),
416 empty_dir: Some(EmptyDirVolumeSource::default()),
417 ..Default::default()
418 });
419
420 volume_mounts.push(VolumeMount {
421 name: VOLUME_DNSSEC_KEYS.to_string(),
422 mount_path: BIND_DNSSEC_KEYS_PATH.to_string(),
423 ..Default::default()
424 });
425 }
426 }
427
428 (volumes, volume_mounts)
429}
430
431#[must_use]
457pub fn build_cluster_labels(cluster_name: &str) -> BTreeMap<String, String> {
458 let mut labels = BTreeMap::new();
459 labels.insert("app".into(), APP_NAME_BIND9.into());
460 labels.insert("cluster".into(), cluster_name.into());
461 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
462 labels.insert(K8S_INSTANCE.into(), cluster_name.into());
463 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_CLUSTER.into());
464 labels.insert(K8S_MANAGED_BY.into(), MANAGED_BY_BIND9_CLUSTER.into());
465 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
466 labels
467}
468
469#[must_use]
490pub fn build_labels_from_instance(
491 instance_name: &str,
492 instance: &Bind9Instance,
493) -> BTreeMap<String, String> {
494 use crate::labels::{BINDY_MANAGED_BY_LABEL, BINDY_ROLE_LABEL};
495
496 let mut labels = BTreeMap::new();
497 labels.insert("app".into(), APP_NAME_BIND9.into());
498 labels.insert("instance".into(), instance_name.into());
499 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
500 labels.insert(K8S_INSTANCE.into(), instance_name.into());
501 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
502 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
503
504 let managed_by = instance
507 .metadata
508 .labels
509 .as_ref()
510 .and_then(|labels| labels.get(BINDY_MANAGED_BY_LABEL))
511 .map_or(MANAGED_BY_BIND9_INSTANCE, String::as_str);
512
513 labels.insert(K8S_MANAGED_BY.into(), managed_by.into());
514
515 if let Some(instance_labels) = &instance.metadata.labels {
518 if let Some(role) = instance_labels.get(BINDY_ROLE_LABEL) {
519 labels.insert(BINDY_ROLE_LABEL.into(), role.clone());
520 }
521 }
522
523 labels
524}
525
526#[must_use]
557pub fn build_pod_labels_from_instance(
558 instance_name: &str,
559 instance: &Bind9Instance,
560) -> BTreeMap<String, String> {
561 use crate::labels::{BINDY_CLUSTER_LABEL, BINDY_ROLE_LABEL, ROLE_PRIMARY, ROLE_SECONDARY};
562
563 let mut labels = build_labels_from_instance(instance_name, instance);
564
565 if !instance.spec.cluster_ref.is_empty() {
566 labels.insert(
567 BINDY_CLUSTER_LABEL.to_string(),
568 instance.spec.cluster_ref.clone(),
569 );
570 }
571
572 labels
573 .entry(BINDY_ROLE_LABEL.to_string())
574 .or_insert_with(|| {
575 match instance.spec.role {
576 crate::crd::ServerRole::Primary => ROLE_PRIMARY,
577 crate::crd::ServerRole::Secondary => ROLE_SECONDARY,
578 }
579 .to_string()
580 });
581
582 labels
583}
584
585#[must_use]
598pub fn build_owner_references(instance: &Bind9Instance) -> Vec<OwnerReference> {
599 vec![OwnerReference {
600 api_version: API_GROUP_VERSION.to_string(),
601 kind: KIND_BIND9_INSTANCE.to_string(),
602 name: instance.name_any(),
603 uid: instance.metadata.uid.clone().unwrap_or_default(),
604 controller: Some(true),
605 block_owner_deletion: Some(true),
606 }]
607}
608
609pub fn build_configmap(
639 name: &str,
640 namespace: &str,
641 instance: &Bind9Instance,
642 cluster: Option<&Bind9Cluster>,
643 role_allow_transfer: Option<&Vec<String>>,
644) -> anyhow::Result<ConfigMap> {
645 debug!(
646 name = %name,
647 namespace = %namespace,
648 "Building ConfigMap for Bind9Instance"
649 );
650
651 let config_map_refs = instance
653 .spec
654 .config_map_refs
655 .as_ref()
656 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
657
658 let named_conf_overridden = config_map_refs.is_some_and(|refs| refs.named_conf.is_some());
659 let options_overridden = config_map_refs.is_some_and(|refs| refs.named_conf_options.is_some());
660
661 let mut data = BTreeMap::new();
663 let labels = build_labels_from_instance(name, instance);
664
665 if !named_conf_overridden {
667 let named_conf = build_named_conf(instance, cluster);
668 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
669 }
670
671 if !options_overridden {
674 let options_conf = build_options_conf(instance, cluster, role_allow_transfer)?;
675 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
676 }
677
678 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
681
682 let owner_refs = build_owner_references(instance);
686
687 Ok(ConfigMap {
688 metadata: ObjectMeta {
689 name: Some(format!("{name}-config")),
690 namespace: Some(namespace.into()),
691 labels: Some(labels),
692 owner_references: Some(owner_refs),
693 ..Default::default()
694 },
695 data: Some(data),
696 ..Default::default()
697 })
698}
699
700pub fn build_cluster_configmap(
720 cluster_name: &str,
721 namespace: &str,
722 cluster: &Bind9Cluster,
723) -> Result<ConfigMap, anyhow::Error> {
724 debug!(
725 cluster_name = %cluster_name,
726 namespace = %namespace,
727 "Building cluster-level shared ConfigMap"
728 );
729
730 let mut data = BTreeMap::new();
732 let labels = build_cluster_labels(cluster_name);
733
734 let named_conf = build_cluster_named_conf(cluster);
736 data.insert(NAMED_CONF_FILENAME.into(), named_conf);
737
738 let options_conf = build_cluster_options_conf(cluster)?;
740 data.insert(NAMED_CONF_OPTIONS_FILENAME.into(), options_conf);
741
742 data.insert(RNDC_CONF_FILENAME.into(), RNDC_CONF_TEMPLATE.to_string());
744
745 Ok(ConfigMap {
746 metadata: ObjectMeta {
747 name: Some(format!("{cluster_name}-config")),
748 namespace: Some(namespace.into()),
749 labels: Some(labels),
750 ..Default::default()
751 },
752 data: Some(data),
753 ..Default::default()
754 })
755}
756
757fn build_named_conf(instance: &Bind9Instance, cluster: Option<&Bind9Cluster>) -> String {
771 let config_map_refs = instance
773 .spec
774 .config_map_refs
775 .as_ref()
776 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()));
777
778 let zones_include = if let Some(refs) = config_map_refs {
779 if refs.named_conf_zones.is_some() {
780 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
782 } else {
783 String::new()
785 }
786 } else {
787 String::new()
789 };
790
791 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
795 let rndc_key_names = "\"bindy-operator\"";
796
797 NAMED_CONF_TEMPLATE
798 .replace("{{ZONES_INCLUDE}}", &zones_include)
799 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
800 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
801}
802
803const DEFAULT_ALLOW_TRANSFER_NONE: &str = "allow-transfer { none; };";
815
816const DEFAULT_RATE_LIMIT_RESPONSES_PER_SECOND: u32 = 15;
822
823#[allow(clippy::too_many_lines)]
845fn build_options_conf(
846 instance: &Bind9Instance,
847 cluster: Option<&Bind9Cluster>,
848 role_allow_transfer: Option<&Vec<String>>,
849) -> anyhow::Result<String> {
850 let recursion;
851 let mut allow_query = String::new();
852 let allow_transfer;
853 let mut dnssec_validate = String::new();
854
855 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
857
858 if let Some(config) = &instance.spec.config {
859 let recursion_value = if let Some(rec) = config.recursion {
861 if rec {
862 "yes"
863 } else {
864 "no"
865 }
866 } else if let Some(global) = global_config {
867 if global.recursion.unwrap_or(false) {
868 "yes"
869 } else {
870 "no"
871 }
872 } else {
873 "no"
874 };
875 recursion = format!("recursion {recursion_value};");
876
877 if let Some(acls) = &config.allow_query {
879 if !acls.is_empty() {
880 let acl_list = build_acl_list(acls)
881 .context("invalid entry in instance spec.config.allow_query")?;
882 allow_query = format!("allow-query {{ {acl_list}; }};");
883 }
884 } else if let Some(global) = global_config {
885 if let Some(global_acls) = &global.allow_query {
886 if !global_acls.is_empty() {
887 let acl_list = build_acl_list(global_acls)
888 .context("invalid entry in cluster spec.global.allow_query")?;
889 allow_query = format!("allow-query {{ {acl_list}; }};");
890 }
891 }
892 }
893
894 if let Some(acls) = &config.allow_transfer {
896 let acl_list = if acls.is_empty() {
898 "none".to_string()
899 } else {
900 build_acl_list(acls)
901 .context("invalid entry in instance spec.config.allow_transfer")?
902 };
903 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
904 } else if let Some(role_acls) = role_allow_transfer {
905 let acl_list = if role_acls.is_empty() {
907 "none".to_string()
908 } else {
909 build_acl_list(role_acls)
910 .context("invalid entry in cluster role-specific allow_transfer")?
911 };
912 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
913 } else if let Some(global) = global_config {
914 if let Some(global_acls) = &global.allow_transfer {
916 let acl_list = if global_acls.is_empty() {
917 "none".to_string()
918 } else {
919 build_acl_list(global_acls)
920 .context("invalid entry in cluster spec.global.allow_transfer")?
921 };
922 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
923 } else {
924 allow_transfer = DEFAULT_ALLOW_TRANSFER_NONE.to_string();
926 }
927 } else {
928 allow_transfer = DEFAULT_ALLOW_TRANSFER_NONE.to_string();
930 }
931
932 if let Some(dnssec) = &config.dnssec {
936 if dnssec.validation.unwrap_or(false) {
937 dnssec_validate = "dnssec-validation yes;".to_string();
938 } else {
939 dnssec_validate = "dnssec-validation no;".to_string();
940 }
941 } else if let Some(global) = global_config {
942 if let Some(global_dnssec) = &global.dnssec {
943 if global_dnssec.validation.unwrap_or(false) {
944 dnssec_validate = "dnssec-validation yes;".to_string();
945 } else {
946 dnssec_validate = "dnssec-validation no;".to_string();
947 }
948 }
949 }
950 } else {
951 if let Some(global) = global_config {
953 let recursion_value = if global.recursion.unwrap_or(false) {
955 "yes"
956 } else {
957 "no"
958 };
959 recursion = format!("recursion {recursion_value};");
960
961 if let Some(acls) = &global.allow_query {
963 if !acls.is_empty() {
964 let acl_list = build_acl_list(acls)
965 .context("invalid entry in cluster spec.global.allow_query")?;
966 allow_query = format!("allow-query {{ {acl_list}; }};");
967 }
968 }
969
970 if let Some(role_acls) = role_allow_transfer {
972 let acl_list = if role_acls.is_empty() {
973 "none".to_string()
974 } else {
975 build_acl_list(role_acls)
976 .context("invalid entry in cluster role-specific allow_transfer")?
977 };
978 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
979 } else if let Some(global_acls) = &global.allow_transfer {
980 let acl_list = if global_acls.is_empty() {
981 "none".to_string()
982 } else {
983 build_acl_list(global_acls)
984 .context("invalid entry in cluster spec.global.allow_transfer")?
985 };
986 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
987 } else {
988 allow_transfer = DEFAULT_ALLOW_TRANSFER_NONE.to_string();
990 }
991
992 if let Some(dnssec) = &global.dnssec {
994 if dnssec.validation.unwrap_or(false) {
995 dnssec_validate = "dnssec-validation yes;".to_string();
996 }
997 }
998 } else {
999 recursion = "recursion no;".to_string();
1001 allow_transfer = DEFAULT_ALLOW_TRANSFER_NONE.to_string();
1003 }
1004 }
1005
1006 let dnssec_policies = generate_dnssec_policies(global_config, instance.spec.config.as_ref())?;
1008
1009 let instance_cfg = instance.spec.config.as_ref();
1011 let forwarders = render_forwarders(
1012 instance_cfg
1013 .and_then(|c| c.forwarders.as_ref())
1014 .or_else(|| global_config.and_then(|g| g.forwarders.as_ref())),
1015 )?;
1016 let listen_on = render_listen_on(
1017 LISTEN_ON_DIRECTIVE,
1018 instance_cfg
1019 .and_then(|c| c.listen_on.as_ref())
1020 .or_else(|| global_config.and_then(|g| g.listen_on.as_ref())),
1021 )?;
1022 let listen_on_v6 = render_listen_on(
1023 LISTEN_ON_V6_DIRECTIVE,
1024 instance_cfg
1025 .and_then(|c| c.listen_on_v6.as_ref())
1026 .or_else(|| global_config.and_then(|g| g.listen_on_v6.as_ref())),
1027 )?;
1028
1029 let rate_limit = render_rate_limit(
1031 instance_cfg
1032 .and_then(|c| c.rate_limit.as_ref())
1033 .or_else(|| global_config.and_then(|g| g.rate_limit.as_ref())),
1034 );
1035
1036 Ok(NAMED_CONF_OPTIONS_TEMPLATE
1038 .replace("{{LISTEN_ON}}", &listen_on)
1039 .replace("{{LISTEN_ON_V6}}", &listen_on_v6)
1040 .replace("{{RECURSION}}", &recursion)
1041 .replace("{{FORWARDERS}}", &forwarders)
1042 .replace("{{ALLOW_QUERY}}", &allow_query)
1043 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
1044 .replace("{{RATE_LIMIT}}", &rate_limit)
1045 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
1046 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies))
1047}
1048
1049fn render_forwarders(forwarders: Option<&Vec<String>>) -> anyhow::Result<String> {
1065 let Some(list) = forwarders else {
1066 return Ok(String::new());
1067 };
1068 if list.is_empty() {
1069 return Ok(String::new());
1070 }
1071
1072 for entry in list {
1073 let trimmed = entry.trim();
1074 if trimmed.parse::<std::net::IpAddr>().is_err() {
1075 anyhow::bail!("invalid forwarder {trimmed:?}: must be a plain IPv4 or IPv6 address");
1076 }
1077 }
1078
1079 let joined = list
1080 .iter()
1081 .map(|entry| entry.trim().to_string())
1082 .collect::<Vec<_>>()
1083 .join("; ");
1084 Ok(format!("forwarders {{ {joined}; }};"))
1085}
1086
1087fn render_rate_limit(rate_limit: Option<&crate::crd::RateLimitConfig>) -> String {
1100 let rps = rate_limit
1101 .and_then(|r| r.responses_per_second)
1102 .unwrap_or(DEFAULT_RATE_LIMIT_RESPONSES_PER_SECOND);
1103 if rps == 0 {
1104 return String::new();
1106 }
1107 format!("rate-limit {{ responses-per-second {rps}; }};")
1108}
1109
1110fn render_listen_on(directive: &str, addresses: Option<&Vec<String>>) -> anyhow::Result<String> {
1126 let list = match addresses {
1127 Some(addrs) if !addrs.is_empty() => build_acl_list(addrs)
1128 .with_context(|| format!("invalid entry in {directive} address list"))?,
1129 _ => LISTEN_ON_DEFAULT.to_string(),
1130 };
1131 Ok(format!(
1132 "{directive} port {DNS_CONTAINER_PORT} {{ {list}; }};"
1133 ))
1134}
1135
1136fn build_cluster_named_conf(cluster: &Bind9Cluster) -> String {
1149 let zones_include = if let Some(refs) = &cluster.spec.common.config_map_refs {
1151 if refs.named_conf_zones.is_some() {
1152 "\n// Include zones file from user-provided ConfigMap\ninclude \"/etc/bind/named.conf.zones\";\n".to_string()
1154 } else {
1155 String::new()
1157 }
1158 } else {
1159 String::new()
1161 };
1162
1163 let rndc_key_includes = "include \"/etc/bind/keys/rndc.key\";";
1167 let rndc_key_names = "\"bindy-operator\"";
1168
1169 NAMED_CONF_TEMPLATE
1170 .replace("{{ZONES_INCLUDE}}", &zones_include)
1171 .replace("{{RNDC_KEY_INCLUDES}}", rndc_key_includes)
1172 .replace("{{RNDC_KEY_NAMES}}", rndc_key_names)
1173}
1174
1175#[allow(clippy::too_many_lines)]
1189fn build_cluster_options_conf(cluster: &Bind9Cluster) -> anyhow::Result<String> {
1190 let recursion;
1191 let mut allow_query = String::new();
1192 let mut allow_transfer = String::new();
1193 let mut dnssec_validate = String::new();
1194
1195 if let Some(global) = &cluster.spec.common.global {
1197 let recursion_value = if global.recursion.unwrap_or(false) {
1199 "yes"
1200 } else {
1201 "no"
1202 };
1203 recursion = format!("recursion {recursion_value};");
1204
1205 if let Some(aq) = &global.allow_query {
1207 if !aq.is_empty() {
1208 let acl_list = build_acl_list(aq)
1209 .context("invalid entry in cluster spec.global.allow_query")?;
1210 allow_query = format!("allow-query {{ {acl_list}; }};");
1211 }
1212 }
1213
1214 if let Some(at) = &global.allow_transfer {
1216 if !at.is_empty() {
1217 let acl_list = build_acl_list(at)
1218 .context("invalid entry in cluster spec.global.allow_transfer")?;
1219 allow_transfer = format!("allow-transfer {{ {acl_list}; }};");
1220 }
1221 }
1222
1223 if let Some(dnssec) = &global.dnssec {
1225 if dnssec.validation.unwrap_or(false) {
1226 dnssec_validate = "dnssec-validation yes;".to_string();
1227 } else {
1228 dnssec_validate = "dnssec-validation no;".to_string();
1229 }
1230 }
1231 } else {
1232 recursion = "recursion no;".to_string();
1234 }
1235
1236 let dnssec_policies = generate_dnssec_policies(cluster.spec.common.global.as_ref(), None)?;
1238
1239 let global = cluster.spec.common.global.as_ref();
1241 let forwarders = render_forwarders(global.and_then(|g| g.forwarders.as_ref()))?;
1242 let listen_on = render_listen_on(
1243 LISTEN_ON_DIRECTIVE,
1244 global.and_then(|g| g.listen_on.as_ref()),
1245 )?;
1246 let listen_on_v6 = render_listen_on(
1247 LISTEN_ON_V6_DIRECTIVE,
1248 global.and_then(|g| g.listen_on_v6.as_ref()),
1249 )?;
1250
1251 let rate_limit = render_rate_limit(global.and_then(|g| g.rate_limit.as_ref()));
1253
1254 Ok(NAMED_CONF_OPTIONS_TEMPLATE
1255 .replace("{{LISTEN_ON}}", &listen_on)
1256 .replace("{{LISTEN_ON_V6}}", &listen_on_v6)
1257 .replace("{{RECURSION}}", &recursion)
1258 .replace("{{FORWARDERS}}", &forwarders)
1259 .replace("{{ALLOW_QUERY}}", &allow_query)
1260 .replace("{{ALLOW_TRANSFER}}", &allow_transfer)
1261 .replace("{{RATE_LIMIT}}", &rate_limit)
1262 .replace("{{DNSSEC_VALIDATE}}", &dnssec_validate)
1263 .replace("{{DNSSEC_POLICIES}}", &dnssec_policies))
1264}
1265
1266#[must_use]
1289struct DeploymentConfig<'a> {
1291 image_config: Option<&'a ImageConfig>,
1292 config_map_refs: Option<&'a ConfigMapRefs>,
1293 version: &'a str,
1294 volumes: Option<&'a Vec<Volume>>,
1295 volume_mounts: Option<&'a Vec<VolumeMount>>,
1296 bindcar_config: Option<&'a crate::crd::BindcarConfig>,
1297 configmap_name: String,
1298}
1299
1300fn resolve_deployment_config<'a>(
1302 name: &str,
1303 instance: &'a Bind9Instance,
1304 cluster: Option<&'a Bind9Cluster>,
1305 cluster_provider: Option<&'a crate::crd::ClusterBind9Provider>,
1306) -> DeploymentConfig<'a> {
1307 let image_config = instance
1309 .spec
1310 .image
1311 .as_ref()
1312 .or_else(|| cluster.and_then(|c| c.spec.common.image.as_ref()))
1313 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.image.as_ref()));
1314
1315 let config_map_refs = instance
1317 .spec
1318 .config_map_refs
1319 .as_ref()
1320 .or_else(|| cluster.and_then(|c| c.spec.common.config_map_refs.as_ref()))
1321 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.config_map_refs.as_ref()));
1322
1323 let version = instance
1325 .spec
1326 .version
1327 .as_deref()
1328 .or_else(|| cluster.and_then(|c| c.spec.common.version.as_deref()))
1329 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.version.as_deref()))
1330 .unwrap_or(DEFAULT_BIND9_VERSION);
1331
1332 let volumes = instance
1334 .spec
1335 .volumes
1336 .as_ref()
1337 .or_else(|| cluster.and_then(|c| c.spec.common.volumes.as_ref()))
1338 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volumes.as_ref()));
1339
1340 let volume_mounts = instance
1342 .spec
1343 .volume_mounts
1344 .as_ref()
1345 .or_else(|| cluster.and_then(|c| c.spec.common.volume_mounts.as_ref()))
1346 .or_else(|| cluster_provider.and_then(|cp| cp.spec.common.volume_mounts.as_ref()));
1347
1348 let bindcar_config = instance
1350 .spec
1351 .bindcar_config
1352 .as_ref()
1353 .or_else(|| {
1354 cluster.and_then(|c| {
1355 c.spec
1356 .common
1357 .global
1358 .as_ref()
1359 .and_then(|g| g.bindcar_config.as_ref())
1360 })
1361 })
1362 .or_else(|| {
1363 cluster_provider.and_then(|cp| {
1364 cp.spec
1365 .common
1366 .global
1367 .as_ref()
1368 .and_then(|g| g.bindcar_config.as_ref())
1369 })
1370 });
1371
1372 let configmap_name = if instance.spec.cluster_ref.is_empty() {
1374 format!("{name}-config")
1376 } else {
1377 format!("{}-config", instance.spec.cluster_ref)
1379 };
1380
1381 DeploymentConfig {
1382 image_config,
1383 config_map_refs,
1384 version,
1385 volumes,
1386 volume_mounts,
1387 bindcar_config,
1388 configmap_name,
1389 }
1390}
1391
1392fn resolve_role_counts(
1403 instance: &Bind9Instance,
1404 cluster: Option<&Bind9Cluster>,
1405 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1406) -> (i32, i32) {
1407 let common = cluster
1408 .map(|c| &c.spec.common)
1409 .or_else(|| cluster_provider.map(|p| &p.spec.common));
1410
1411 let Some(common) = common else {
1412 return (1, 1);
1413 };
1414
1415 let primaries = common
1416 .primary
1417 .as_ref()
1418 .and_then(|p| p.replicas)
1419 .unwrap_or(0);
1420 let secondaries = common
1421 .secondary
1422 .as_ref()
1423 .and_then(|s| s.replicas)
1424 .unwrap_or(0);
1425
1426 let role_count = match instance.spec.role {
1427 crate::crd::ServerRole::Primary => primaries,
1428 crate::crd::ServerRole::Secondary => secondaries,
1429 };
1430
1431 (role_count.max(1), (primaries + secondaries).max(1))
1432}
1433
1434pub fn build_deployment(
1435 name: &str,
1436 namespace: &str,
1437 instance: &Bind9Instance,
1438 cluster: Option<&Bind9Cluster>,
1439 cluster_provider: Option<&crate::crd::ClusterBind9Provider>,
1440 rndc_secret_name: &str,
1441) -> Deployment {
1442 debug!(
1443 name = %name,
1444 namespace = %namespace,
1445 has_cluster = cluster.is_some(),
1446 has_cluster_provider = cluster_provider.is_some(),
1447 "Building Deployment for Bind9Instance"
1448 );
1449
1450 let selector_labels = build_labels_from_instance(name, instance);
1454 let pod_labels = build_pod_labels_from_instance(name, instance);
1455 let replicas = instance.spec.replicas.unwrap_or(1);
1456 debug!(replicas, "Deployment replica count");
1457
1458 let (role_instance_count, cluster_instance_count) =
1461 resolve_role_counts(instance, cluster, cluster_provider);
1462 let placement_config = crate::placement::resolve_placement(instance, cluster, cluster_provider);
1463 let placement_ctx = crate::placement::PlacementContext {
1464 instance_name: name,
1465 cluster_name: (!instance.spec.cluster_ref.is_empty())
1466 .then_some(instance.spec.cluster_ref.as_str()),
1467 role: instance.spec.role,
1468 instance_replicas: replicas,
1469 role_instance_count,
1470 cluster_instance_count,
1471 instance_selector_labels: &selector_labels,
1472 };
1473 let placement = crate::placement::build_pod_placement(placement_config, &placement_ctx);
1474
1475 let config = resolve_deployment_config(name, instance, cluster, cluster_provider);
1476
1477 let owner_refs = build_owner_references(instance);
1478
1479 let global_config = cluster.and_then(|c| c.spec.common.global.as_ref());
1481 let instance_config = instance.spec.config.as_ref();
1482
1483 let (dnssec_volumes, dnssec_volume_mounts) =
1485 build_dnssec_key_volumes(global_config, instance_config);
1486
1487 let all_volumes = if dnssec_volumes.is_empty() {
1489 config.volumes.map(std::borrow::ToOwned::to_owned)
1490 } else {
1491 let mut merged = dnssec_volumes;
1492 if let Some(custom) = config.volumes {
1493 merged.extend(custom.iter().cloned());
1494 }
1495 Some(merged)
1496 };
1497
1498 let all_volume_mounts = if dnssec_volume_mounts.is_empty() {
1500 config.volume_mounts.map(std::borrow::ToOwned::to_owned)
1501 } else {
1502 let mut merged = dnssec_volume_mounts;
1503 if let Some(custom) = config.volume_mounts {
1504 merged.extend(custom.iter().cloned());
1505 }
1506 Some(merged)
1507 };
1508
1509 Deployment {
1510 metadata: ObjectMeta {
1511 name: Some(name.into()),
1512 namespace: Some(namespace.into()),
1513 labels: Some(selector_labels.clone()),
1518 owner_references: Some(owner_refs),
1519 ..Default::default()
1520 },
1521 spec: Some(DeploymentSpec {
1522 replicas: Some(replicas),
1523 selector: LabelSelector {
1527 match_labels: Some(selector_labels.clone()),
1528 ..Default::default()
1529 },
1530 template: PodTemplateSpec {
1531 metadata: Some(ObjectMeta {
1532 labels: Some(pod_labels.clone()),
1533 ..Default::default()
1534 }),
1535 spec: Some(build_pod_spec(
1536 &config.configmap_name,
1537 rndc_secret_name,
1538 config.version,
1539 config.image_config,
1540 config.config_map_refs,
1541 all_volumes.as_ref(),
1542 all_volume_mounts.as_ref(),
1543 config.bindcar_config,
1544 &placement,
1545 )),
1546 },
1547 ..Default::default()
1548 }),
1549 ..Default::default()
1550 }
1551}
1552
1553#[allow(clippy::too_many_arguments)]
1566#[allow(clippy::too_many_lines)]
1567fn build_pod_spec(
1568 configmap_name: &str,
1569 rndc_secret_name: &str,
1570 version: &str,
1571 image_config: Option<&ImageConfig>,
1572 config_map_refs: Option<&ConfigMapRefs>,
1573 custom_volumes: Option<&Vec<Volume>>,
1574 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1575 bindcar_config: Option<&crate::crd::BindcarConfig>,
1576 placement: &crate::placement::ResolvedPlacement,
1577) -> PodSpec {
1578 let image = if let Some(img_cfg) = image_config {
1580 img_cfg
1581 .image
1582 .clone()
1583 .unwrap_or_else(|| format!("internetsystemsconsortium/bind9:{version}"))
1584 } else {
1585 format!("internetsystemsconsortium/bind9:{version}")
1586 };
1587
1588 let image_pull_policy = image_config
1590 .and_then(|cfg| cfg.image_pull_policy.clone())
1591 .unwrap_or_else(|| "IfNotPresent".into());
1592
1593 let bind9_container = Container {
1595 name: CONTAINER_NAME_BIND9.into(),
1596 image: Some(image),
1597 image_pull_policy: Some(image_pull_policy),
1598 command: Some(vec!["named".into()]),
1599 args: Some(vec![
1600 "-c".into(),
1601 BIND_NAMED_CONF_PATH.into(),
1602 "-g".into(), ]),
1604 ports: Some(vec![
1605 ContainerPort {
1606 name: Some("dns-tcp".into()),
1607 container_port: i32::from(DNS_CONTAINER_PORT),
1608 protocol: Some("TCP".into()),
1609 ..Default::default()
1610 },
1611 ContainerPort {
1612 name: Some("dns-udp".into()),
1613 container_port: i32::from(DNS_CONTAINER_PORT),
1614 protocol: Some("UDP".into()),
1615 ..Default::default()
1616 },
1617 ContainerPort {
1618 name: Some("rndc".into()),
1619 container_port: i32::from(RNDC_PORT),
1620 protocol: Some("TCP".into()),
1621 ..Default::default()
1622 },
1623 ]),
1624 env: Some(vec![
1625 EnvVar {
1626 name: "TZ".into(),
1627 value: Some("UTC".into()),
1628 ..Default::default()
1629 },
1630 EnvVar {
1631 name: "MALLOC_CONF".into(),
1632 value: Some(BIND9_MALLOC_CONF.into()),
1633 ..Default::default()
1634 },
1635 ]),
1636 volume_mounts: Some(build_volume_mounts(config_map_refs, custom_volume_mounts)),
1637 liveness_probe: Some(Probe {
1638 tcp_socket: Some(TCPSocketAction {
1639 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1640 ..Default::default()
1641 }),
1642 initial_delay_seconds: Some(LIVENESS_INITIAL_DELAY_SECS),
1643 period_seconds: Some(LIVENESS_PERIOD_SECS),
1644 timeout_seconds: Some(LIVENESS_TIMEOUT_SECS),
1645 failure_threshold: Some(LIVENESS_FAILURE_THRESHOLD),
1646 ..Default::default()
1647 }),
1648 readiness_probe: Some(Probe {
1649 tcp_socket: Some(TCPSocketAction {
1650 port: IntOrString::Int(i32::from(DNS_CONTAINER_PORT)),
1651 ..Default::default()
1652 }),
1653 initial_delay_seconds: Some(READINESS_INITIAL_DELAY_SECS),
1654 period_seconds: Some(READINESS_PERIOD_SECS),
1655 timeout_seconds: Some(READINESS_TIMEOUT_SECS),
1656 failure_threshold: Some(READINESS_FAILURE_THRESHOLD),
1657 ..Default::default()
1658 }),
1659 security_context: Some(SecurityContext {
1660 run_as_non_root: Some(true),
1661 run_as_user: Some(BIND9_NONROOT_UID),
1662 run_as_group: Some(BIND9_NONROOT_UID),
1663 allow_privilege_escalation: Some(false),
1664 capabilities: Some(Capabilities {
1665 drop: Some(vec!["ALL".to_string()]),
1670 add: None,
1671 }),
1672 seccomp_profile: Some(SeccompProfile {
1676 type_: "RuntimeDefault".to_string(),
1677 ..Default::default()
1678 }),
1679 ..Default::default()
1680 }),
1681 ..Default::default()
1682 };
1683
1684 let image_pull_secrets = image_config.and_then(|cfg| {
1686 cfg.image_pull_secrets.as_ref().map(|secrets| {
1687 secrets
1688 .iter()
1689 .map(|s| k8s_openapi::api::core::v1::LocalObjectReference { name: s.clone() })
1690 .collect()
1691 })
1692 });
1693
1694 PodSpec {
1695 containers: {
1696 let mut containers = vec![bind9_container];
1697 containers.push(build_api_sidecar_container(
1698 bindcar_config,
1699 rndc_secret_name,
1700 ));
1701 containers
1702 },
1703 volumes: Some(build_volumes(
1704 configmap_name,
1705 rndc_secret_name,
1706 config_map_refs,
1707 custom_volumes,
1708 )),
1709 image_pull_secrets,
1710 service_account_name: Some(BIND9_SERVICE_ACCOUNT.into()),
1711 topology_spread_constraints: placement.topology_spread_constraints.clone(),
1714 security_context: Some(PodSecurityContext {
1715 run_as_user: Some(BIND9_NONROOT_UID),
1716 run_as_group: Some(BIND9_NONROOT_UID),
1717 fs_group: Some(BIND9_NONROOT_UID),
1718 run_as_non_root: Some(true),
1719 seccomp_profile: Some(SeccompProfile {
1723 type_: "RuntimeDefault".to_string(),
1724 ..Default::default()
1725 }),
1726 ..Default::default()
1727 }),
1728 ..Default::default()
1729 }
1730}
1731
1732#[allow(clippy::too_many_lines)]
1743fn build_api_sidecar_container(
1744 bindcar_config: Option<&crate::crd::BindcarConfig>,
1745 rndc_secret_name: &str,
1746) -> Container {
1747 let image = bindcar_config
1749 .and_then(|c| c.image.clone())
1750 .unwrap_or_else(|| crate::constants::DEFAULT_BINDCAR_IMAGE.to_string());
1751
1752 let image_pull_policy = bindcar_config
1753 .and_then(|c| c.image_pull_policy.clone())
1754 .unwrap_or_else(|| "IfNotPresent".to_string());
1755
1756 let port = bindcar_config
1757 .and_then(|c| c.port)
1758 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
1759
1760 let log_level = bindcar_config
1761 .and_then(|c| c.log_level.clone())
1762 .unwrap_or_else(|| "info".to_string());
1763
1764 let resources = bindcar_config.and_then(|c| c.resources.clone());
1765
1766 let operator_namespace = std::env::var("POD_NAMESPACE")
1772 .unwrap_or_else(|_| crate::constants::DEFAULT_OPERATOR_NAMESPACE.to_string());
1773 let allowed_service_account = format!(
1774 "system:serviceaccount:{operator_namespace}:{}",
1775 crate::constants::OPERATOR_SERVICE_ACCOUNT
1776 );
1777
1778 let mut env_vars = vec![
1780 EnvVar {
1781 name: "BIND_ZONE_DIR".into(),
1782 value: Some(BIND_CACHE_PATH.into()),
1783 ..Default::default()
1784 },
1785 EnvVar {
1786 name: "API_PORT".into(),
1787 value: Some(port.to_string()),
1788 ..Default::default()
1789 },
1790 EnvVar {
1791 name: "RUST_LOG".into(),
1792 value: Some(log_level),
1793 ..Default::default()
1794 },
1795 EnvVar {
1796 name: "BIND_ALLOWED_SERVICE_ACCOUNTS".into(),
1797 value: Some(allowed_service_account),
1798 ..Default::default()
1799 },
1800 EnvVar {
1804 name: "BIND_TOKEN_AUDIENCES".into(),
1805 value: Some(crate::constants::BINDCAR_TOKEN_AUDIENCE.into()),
1806 ..Default::default()
1807 },
1808 EnvVar {
1811 name: "TMPDIR".into(),
1812 value: Some(crate::constants::BINDCAR_TMP_PATH.into()),
1813 ..Default::default()
1814 },
1815 EnvVar {
1816 name: "RNDC_SECRET".into(),
1817 value_from: Some(EnvVarSource {
1818 secret_key_ref: Some(SecretKeySelector {
1819 name: rndc_secret_name.to_string(),
1820 key: "secret".to_string(),
1821 optional: Some(false),
1822 }),
1823 ..Default::default()
1824 }),
1825 ..Default::default()
1826 },
1827 EnvVar {
1828 name: "RNDC_ALGORITHM".into(),
1829 value_from: Some(EnvVarSource {
1830 secret_key_ref: Some(SecretKeySelector {
1831 name: rndc_secret_name.to_string(),
1832 key: "algorithm".to_string(),
1833 optional: Some(false),
1834 }),
1835 ..Default::default()
1836 }),
1837 ..Default::default()
1838 },
1839 EnvVar {
1844 name: "NSUPDATE_PORT".into(),
1845 value: Some(DNS_CONTAINER_PORT.to_string()),
1846 ..Default::default()
1847 },
1848 ];
1849
1850 if let Some(config) = bindcar_config {
1852 if let Some(user_env_vars) = &config.env_vars {
1853 env_vars.extend(user_env_vars.clone());
1854 }
1855 }
1856
1857 Container {
1858 name: CONTAINER_NAME_BINDCAR.into(),
1859 image: Some(image),
1860 image_pull_policy: Some(image_pull_policy),
1861 ports: Some(vec![ContainerPort {
1862 name: Some("http".into()),
1863 container_port: port,
1864 protocol: Some("TCP".into()),
1865 ..Default::default()
1866 }]),
1867 env: Some(env_vars),
1868 volume_mounts: Some(vec![
1869 VolumeMount {
1870 name: VOLUME_CACHE.into(),
1871 mount_path: BIND_CACHE_PATH.into(),
1872 ..Default::default()
1873 },
1874 VolumeMount {
1875 name: VOLUME_RNDC_KEY.into(),
1876 mount_path: BIND_KEYS_PATH.into(),
1877 read_only: Some(true),
1878 ..Default::default()
1879 },
1880 VolumeMount {
1881 name: VOLUME_CONFIG.into(),
1882 mount_path: BIND_RNDC_CONF_PATH.into(),
1883 sub_path: Some(RNDC_CONF_FILENAME.into()),
1884 ..Default::default()
1885 },
1886 VolumeMount {
1889 name: VOLUME_TMP.into(),
1890 mount_path: crate::constants::BINDCAR_TMP_PATH.into(),
1891 ..Default::default()
1892 },
1893 ]),
1894 resources,
1895 security_context: Some(SecurityContext {
1896 run_as_non_root: Some(true),
1897 run_as_user: Some(BIND9_NONROOT_UID),
1898 run_as_group: Some(BIND9_NONROOT_UID),
1899 allow_privilege_escalation: Some(false),
1900 read_only_root_filesystem: Some(true),
1904 capabilities: Some(Capabilities {
1905 drop: Some(vec!["ALL".to_string()]),
1906 ..Default::default()
1907 }),
1908 seccomp_profile: Some(SeccompProfile {
1909 type_: "RuntimeDefault".to_string(),
1910 ..Default::default()
1911 }),
1912 ..Default::default()
1913 }),
1914 ..Default::default()
1915 }
1916}
1917
1918fn build_volume_mounts(
1936 config_map_refs: Option<&ConfigMapRefs>,
1937 custom_volume_mounts: Option<&Vec<VolumeMount>>,
1938) -> Vec<VolumeMount> {
1939 let mut mounts = vec![
1940 VolumeMount {
1941 name: VOLUME_ZONES.into(),
1942 mount_path: BIND_ZONES_PATH.into(),
1943 ..Default::default()
1944 },
1945 VolumeMount {
1946 name: VOLUME_CACHE.into(),
1947 mount_path: BIND_CACHE_PATH.into(),
1948 ..Default::default()
1949 },
1950 VolumeMount {
1951 name: VOLUME_RNDC_KEY.into(),
1952 mount_path: BIND_KEYS_PATH.into(),
1953 read_only: Some(true),
1954 ..Default::default()
1955 },
1956 ];
1957
1958 if let Some(refs) = config_map_refs {
1960 if let Some(_configmap_name) = &refs.named_conf {
1961 mounts.push(VolumeMount {
1962 name: VOLUME_NAMED_CONF.into(),
1963 mount_path: BIND_NAMED_CONF_PATH.into(),
1964 sub_path: Some(NAMED_CONF_FILENAME.into()),
1965 ..Default::default()
1966 });
1967 } else {
1968 mounts.push(VolumeMount {
1970 name: VOLUME_CONFIG.into(),
1971 mount_path: BIND_NAMED_CONF_PATH.into(),
1972 sub_path: Some(NAMED_CONF_FILENAME.into()),
1973 ..Default::default()
1974 });
1975 }
1976
1977 if let Some(_configmap_name) = &refs.named_conf_options {
1978 mounts.push(VolumeMount {
1979 name: VOLUME_NAMED_CONF_OPTIONS.into(),
1980 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1981 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1982 ..Default::default()
1983 });
1984 } else {
1985 mounts.push(VolumeMount {
1987 name: VOLUME_CONFIG.into(),
1988 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
1989 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
1990 ..Default::default()
1991 });
1992 }
1993
1994 if let Some(_configmap_name) = &refs.named_conf_zones {
1996 mounts.push(VolumeMount {
1997 name: VOLUME_NAMED_CONF_ZONES.into(),
1998 mount_path: BIND_NAMED_CONF_ZONES_PATH.into(),
1999 sub_path: Some(NAMED_CONF_ZONES_FILENAME.into()),
2000 ..Default::default()
2001 });
2002 }
2003 } else {
2005 mounts.push(VolumeMount {
2007 name: VOLUME_CONFIG.into(),
2008 mount_path: BIND_NAMED_CONF_PATH.into(),
2009 sub_path: Some(NAMED_CONF_FILENAME.into()),
2010 ..Default::default()
2011 });
2012 mounts.push(VolumeMount {
2013 name: VOLUME_CONFIG.into(),
2014 mount_path: BIND_NAMED_CONF_OPTIONS_PATH.into(),
2015 sub_path: Some(NAMED_CONF_OPTIONS_FILENAME.into()),
2016 ..Default::default()
2017 });
2018 }
2020
2021 mounts.push(VolumeMount {
2023 name: VOLUME_CONFIG.into(),
2024 mount_path: BIND_RNDC_CONF_PATH.into(),
2025 sub_path: Some(RNDC_CONF_FILENAME.into()),
2026 ..Default::default()
2027 });
2028
2029 if let Some(custom_mounts) = custom_volume_mounts {
2031 mounts.extend(custom_mounts.iter().cloned());
2032 }
2033
2034 mounts
2035}
2036
2037fn build_volumes(
2062 configmap_name: &str,
2063 rndc_secret_name: &str,
2064 config_map_refs: Option<&ConfigMapRefs>,
2065 custom_volumes: Option<&Vec<Volume>>,
2066) -> Vec<Volume> {
2067 let mut volumes = vec![
2068 Volume {
2069 name: VOLUME_ZONES.into(),
2070 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
2071 ..Default::default()
2072 },
2073 Volume {
2074 name: VOLUME_CACHE.into(),
2075 empty_dir: Some(k8s_openapi::api::core::v1::EmptyDirVolumeSource::default()),
2076 ..Default::default()
2077 },
2078 Volume {
2079 name: VOLUME_RNDC_KEY.into(),
2080 secret: Some(k8s_openapi::api::core::v1::SecretVolumeSource {
2081 secret_name: Some(rndc_secret_name.to_string()),
2082 ..Default::default()
2083 }),
2084 ..Default::default()
2085 },
2086 Volume {
2090 name: VOLUME_TMP.into(),
2091 empty_dir: Some(EmptyDirVolumeSource {
2092 medium: Some("Memory".to_string()),
2093 ..Default::default()
2094 }),
2095 ..Default::default()
2096 },
2097 ];
2098
2099 if let Some(refs) = config_map_refs {
2101 if let Some(configmap_name) = &refs.named_conf {
2102 volumes.push(Volume {
2103 name: VOLUME_NAMED_CONF.into(),
2104 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
2105 name: configmap_name.clone(),
2106 ..Default::default()
2107 }),
2108 ..Default::default()
2109 });
2110 }
2111
2112 if let Some(configmap_name) = &refs.named_conf_options {
2113 volumes.push(Volume {
2114 name: VOLUME_NAMED_CONF_OPTIONS.into(),
2115 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
2116 name: configmap_name.clone(),
2117 ..Default::default()
2118 }),
2119 ..Default::default()
2120 });
2121 }
2122
2123 if let Some(configmap_name) = &refs.named_conf_zones {
2124 volumes.push(Volume {
2125 name: VOLUME_NAMED_CONF_ZONES.into(),
2126 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
2127 name: configmap_name.clone(),
2128 ..Default::default()
2129 }),
2130 ..Default::default()
2131 });
2132 }
2133 }
2134
2135 volumes.push(Volume {
2141 name: VOLUME_CONFIG.into(),
2142 config_map: Some(k8s_openapi::api::core::v1::ConfigMapVolumeSource {
2143 name: configmap_name.to_string(),
2144 ..Default::default()
2145 }),
2146 ..Default::default()
2147 });
2148
2149 if let Some(custom_vols) = custom_volumes {
2151 volumes.extend(custom_vols.iter().cloned());
2152 }
2153
2154 volumes
2155}
2156
2157#[must_use]
2199pub fn build_service(
2200 name: &str,
2201 namespace: &str,
2202 instance: &Bind9Instance,
2203 custom_config: Option<&crate::crd::ServiceConfig>,
2204) -> Service {
2205 let labels = build_labels_from_instance(name, instance);
2207 let owner_refs = build_owner_references(instance);
2208
2209 let api_container_port = instance
2211 .spec
2212 .bindcar_config
2213 .as_ref()
2214 .and_then(|c| c.port)
2215 .unwrap_or(i32::from(crate::constants::BINDCAR_API_PORT));
2216
2217 let mut default_spec = ServiceSpec {
2219 selector: Some(labels.clone()),
2220 ports: Some(vec![
2221 ServicePort {
2222 name: Some("dns-tcp".into()),
2223 port: i32::from(DNS_PORT),
2224 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
2225 protocol: Some("TCP".into()),
2226 ..Default::default()
2227 },
2228 ServicePort {
2229 name: Some("dns-udp".into()),
2230 port: i32::from(DNS_PORT),
2231 target_port: Some(IntOrString::Int(i32::from(DNS_CONTAINER_PORT))),
2232 protocol: Some("UDP".into()),
2233 ..Default::default()
2234 },
2235 ServicePort {
2236 name: Some("http".into()),
2237 port: i32::from(crate::constants::BINDCAR_SERVICE_PORT),
2238 target_port: Some(IntOrString::Int(api_container_port)),
2239 protocol: Some("TCP".into()),
2240 ..Default::default()
2241 },
2242 ]),
2243 type_: Some("ClusterIP".into()),
2244 ..Default::default()
2245 };
2246
2247 if let Some(bindcar_service_spec) = instance
2249 .spec
2250 .bindcar_config
2251 .as_ref()
2252 .and_then(|c| c.service_spec.as_ref())
2253 {
2254 merge_service_spec(&mut default_spec, bindcar_service_spec);
2255 }
2256
2257 let (custom_spec, custom_annotations) = custom_config.map_or((None, None), |config| {
2259 (config.spec.as_ref(), config.annotations.as_ref())
2260 });
2261
2262 if let Some(custom) = custom_spec {
2264 merge_service_spec(&mut default_spec, custom);
2265 }
2266
2267 let mut metadata = ObjectMeta {
2269 name: Some(name.into()),
2270 namespace: Some(namespace.into()),
2271 labels: Some(labels),
2272 owner_references: Some(owner_refs),
2273 ..Default::default()
2274 };
2275
2276 if let Some(annotations) = custom_annotations {
2278 metadata.annotations = Some(annotations.clone());
2279 }
2280
2281 Service {
2282 metadata,
2283 spec: Some(default_spec),
2284 ..Default::default()
2285 }
2286}
2287
2288#[must_use]
2315pub fn build_service_account(namespace: &str, _instance: &Bind9Instance) -> ServiceAccount {
2316 let mut labels = BTreeMap::new();
2323 labels.insert(K8S_NAME.into(), APP_NAME_BIND9.into());
2324 labels.insert(K8S_COMPONENT.into(), COMPONENT_DNS_SERVER.into());
2325 labels.insert(K8S_PART_OF.into(), PART_OF_BINDY.into());
2326
2327 ServiceAccount {
2328 metadata: ObjectMeta {
2329 name: Some(BIND9_SERVICE_ACCOUNT.into()),
2330 namespace: Some(namespace.into()),
2331 labels: Some(labels),
2332 owner_references: None, ..Default::default()
2334 },
2335 ..Default::default()
2336 }
2337}
2338
2339fn merge_service_spec(default: &mut ServiceSpec, custom: &ServiceSpec) {
2347 if let Some(ref type_) = custom.type_ {
2349 default.type_ = Some(type_.clone());
2350 }
2351
2352 if let Some(ref lb_ip) = custom.load_balancer_ip {
2354 default.load_balancer_ip = Some(lb_ip.clone());
2355 }
2356
2357 if let Some(ref affinity) = custom.session_affinity {
2359 default.session_affinity = Some(affinity.clone());
2360 }
2361
2362 if let Some(ref config) = custom.session_affinity_config {
2364 default.session_affinity_config = Some(config.clone());
2365 }
2366
2367 if let Some(ref cluster_ip) = custom.cluster_ip {
2369 default.cluster_ip = Some(cluster_ip.clone());
2370 }
2371
2372 if let Some(ref policy) = custom.external_traffic_policy {
2374 default.external_traffic_policy = Some(policy.clone());
2375 }
2376
2377 if let Some(ref ranges) = custom.load_balancer_source_ranges {
2379 default.load_balancer_source_ranges = Some(ranges.clone());
2380 }
2381
2382 if let Some(ref ips) = custom.external_ips {
2384 default.external_ips = Some(ips.clone());
2385 }
2386
2387 if let Some(ref class) = custom.load_balancer_class {
2389 default.load_balancer_class = Some(class.clone());
2390 }
2391
2392 if let Some(port) = custom.health_check_node_port {
2394 default.health_check_node_port = Some(port);
2395 }
2396
2397 if let Some(publish) = custom.publish_not_ready_addresses {
2399 default.publish_not_ready_addresses = Some(publish);
2400 }
2401
2402 if let Some(allocate) = custom.allocate_load_balancer_node_ports {
2404 default.allocate_load_balancer_node_ports = Some(allocate);
2405 }
2406
2407 if let Some(ref policy) = custom.internal_traffic_policy {
2409 default.internal_traffic_policy = Some(policy.clone());
2410 }
2411
2412 if let Some(ref families) = custom.ip_families {
2414 default.ip_families = Some(families.clone());
2415 }
2416
2417 if let Some(ref policy) = custom.ip_family_policy {
2419 default.ip_family_policy = Some(policy.clone());
2420 }
2421
2422 if let Some(ref ips) = custom.cluster_ips {
2424 default.cluster_ips = Some(ips.clone());
2425 }
2426
2427 if let Some(ref custom_ports) = custom.ports {
2429 if let Some(ref mut default_ports) = default.ports {
2430 for custom_port in custom_ports {
2432 if let Some(existing_port) = default_ports
2433 .iter_mut()
2434 .find(|p| p.name == custom_port.name)
2435 {
2436 *existing_port = custom_port.clone();
2438 } else {
2439 default_ports.push(custom_port.clone());
2441 }
2442 }
2443 } else {
2444 default.ports = Some(custom_ports.clone());
2446 }
2447 }
2448
2449 }