|
1 | | -use std::{env, path::PathBuf, str::FromStr, sync::OnceLock}; |
| 1 | +use std::{env, str::FromStr}; |
2 | 2 |
|
3 | 3 | use snafu::{ResultExt, Snafu}; |
4 | 4 | use tracing::instrument; |
5 | 5 |
|
6 | 6 | use crate::commons::networking::DomainName; |
7 | 7 |
|
8 | 8 | const KUBERNETES_CLUSTER_DOMAIN_ENV: &str = "KUBERNETES_CLUSTER_DOMAIN"; |
9 | | -const KUBERNETES_SERVICE_HOST_ENV: &str = "KUBERNETES_SERVICE_HOST"; |
10 | | - |
11 | 9 | const KUBERNETES_CLUSTER_DOMAIN_DEFAULT: &str = "cluster.local"; |
12 | | -const RESOLVE_CONF_FILE_PATH: &str = "/etc/resolv.conf"; |
13 | 10 |
|
14 | 11 | #[derive(Debug, Snafu)] |
15 | 12 | pub enum Error { |
16 | | - #[snafu(display("failed to read resolv config from {RESOLVE_CONF_FILE_PATH:?}"))] |
17 | | - ReadResolvConfFile { source: std::io::Error }, |
18 | | - |
19 | 13 | #[snafu(display("failed to parse {cluster_domain:?} as domain name"))] |
20 | 14 | ParseDomainName { |
21 | 15 | source: crate::validation::Errors, |
22 | 16 | cluster_domain: String, |
23 | 17 | }, |
24 | | - |
25 | | - #[snafu(display(r#"unable to find "search" entry"#))] |
26 | | - NoSearchEntry, |
27 | | - |
28 | | - #[snafu(display(r#"unable to find unambiguous domain in "search" entry"#))] |
29 | | - AmbiguousDomainEntries, |
30 | 18 | } |
31 | 19 |
|
32 | 20 | /// Tries to retrieve the Kubernetes cluster domain. |
33 | 21 | /// |
34 | | -/// 1. Return `KUBERNETES_CLUSTER_DOMAIN` if set, otherwise |
35 | | -/// 2. Return the cluster domain parsed from the `/etc/resolv.conf` file if `KUBERNETES_SERVICE_HOST` |
36 | | -/// is set, otherwise fall back to `cluster.local`. |
37 | | -/// |
38 | | -/// This variable is initialized in [`crate::client::initialize_operator`], which is called in the |
39 | | -/// main function. It can be used as suggested below. |
40 | | -/// |
41 | | -/// ## Usage |
42 | | -/// |
43 | | -/// ```no_run |
44 | | -/// use stackable_operator::utils::cluster_domain::KUBERNETES_CLUSTER_DOMAIN; |
45 | | -/// |
46 | | -/// let kubernetes_cluster_domain = KUBERNETES_CLUSTER_DOMAIN.get() |
47 | | -/// .expect("KUBERNETES_CLUSTER_DOMAIN must first be set by calling initialize_operator"); |
48 | | -/// |
49 | | -/// tracing::info!(%kubernetes_cluster_domain, "Found cluster domain"); |
50 | | -/// ``` |
51 | | -/// |
52 | | -/// ## See |
53 | | -/// |
54 | | -/// - <https://github.com/stackabletech/issues/issues/436> |
55 | | -/// - <https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/> |
56 | | -pub static KUBERNETES_CLUSTER_DOMAIN: OnceLock<DomainName> = OnceLock::new(); |
57 | | - |
| 22 | +/// Return `KUBERNETES_CLUSTER_DOMAIN` if set, otherwise default to |
| 23 | +/// [`KUBERNETES_CLUSTER_DOMAIN_DEFAULT`]. |
58 | 24 | #[instrument] |
59 | 25 | pub(crate) fn retrieve_cluster_domain() -> Result<DomainName, Error> { |
60 | | - // 1. Read KUBERNETES_CLUSTER_DOMAIN env var |
61 | 26 | tracing::debug!("Trying to determine the Kubernetes cluster domain..."); |
62 | 27 |
|
63 | | - match env::var(KUBERNETES_CLUSTER_DOMAIN_ENV) { |
| 28 | + Ok(match env::var(KUBERNETES_CLUSTER_DOMAIN_ENV) { |
64 | 29 | Ok(cluster_domain) if !cluster_domain.is_empty() => { |
65 | 30 | let cluster_domain = DomainName::from_str(&cluster_domain) |
66 | 31 | .context(ParseDomainNameSnafu { cluster_domain })?; |
67 | 32 | tracing::info!( |
68 | 33 | %cluster_domain, |
69 | 34 | "Using Kubernetes cluster domain from {KUBERNETES_CLUSTER_DOMAIN_ENV:?} environment variable" |
70 | 35 | ); |
71 | | - return Ok(cluster_domain); |
72 | | - } |
73 | | - _ => {} |
74 | | - }; |
75 | | - |
76 | | - // 2. If no env var is set, check if we run in a clustered (Kubernetes/Openshift) environment |
77 | | - // by checking if KUBERNETES_SERVICE_HOST is set: If not default to 'cluster.local'. |
78 | | - tracing::debug!( |
79 | | - "Trying to determine the operator runtime environment as environment variable \ |
80 | | - {KUBERNETES_CLUSTER_DOMAIN_ENV:?} is not set" |
81 | | - ); |
82 | | - |
83 | | - match env::var(KUBERNETES_SERVICE_HOST_ENV) { |
84 | | - Ok(_) => { |
85 | | - let cluster_domain = retrieve_cluster_domain_from_resolv_conf(RESOLVE_CONF_FILE_PATH)?; |
86 | | - let cluster_domain = DomainName::from_str(&cluster_domain) |
87 | | - .context(ParseDomainNameSnafu { cluster_domain })?; |
88 | | - |
89 | | - tracing::info!( |
90 | | - %cluster_domain, |
91 | | - "Using Kubernetes cluster domain from {RESOLVE_CONF_FILE_PATH:?} file" |
92 | | - ); |
93 | | - |
94 | | - Ok(cluster_domain) |
| 36 | + cluster_domain |
95 | 37 | } |
96 | | - Err(_) => { |
| 38 | + _ => { |
97 | 39 | let cluster_domain = DomainName::from_str(KUBERNETES_CLUSTER_DOMAIN_DEFAULT) |
98 | 40 | .expect("KUBERNETES_CLUSTER_DOMAIN_DEFAULT constant must a valid domain"); |
99 | | - |
100 | 41 | tracing::info!( |
101 | 42 | %cluster_domain, |
102 | | - "Could not determine Kubernetes cluster domain as the operator is not running within Kubernetes, assuming default Kubernetes cluster domain" |
| 43 | + "Using default Kubernetes cluster domain as {KUBERNETES_CLUSTER_DOMAIN_ENV:?} environment variable is not set" |
103 | 44 | ); |
104 | | - |
105 | | - Ok(cluster_domain) |
| 45 | + cluster_domain |
106 | 46 | } |
107 | | - } |
108 | | -} |
109 | | - |
110 | | -#[instrument] |
111 | | -fn retrieve_cluster_domain_from_resolv_conf( |
112 | | - path: impl Into<PathBuf> + std::fmt::Debug, |
113 | | -) -> Result<String, Error> { |
114 | | - let path = path.into(); |
115 | | - let content = std::fs::read_to_string(&path) |
116 | | - .inspect_err(|error| { |
117 | | - tracing::error!(%error, path = %path.display(), "Cannot read resolv conf"); |
118 | | - }) |
119 | | - .context(ReadResolvConfFileSnafu)?; |
120 | | - |
121 | | - // If there are multiple search directives, only the search |
122 | | - // man 5 resolv.conf |
123 | | - let Some(last_search_entry) = content |
124 | | - .lines() |
125 | | - .rev() |
126 | | - .map(|l| l.trim()) |
127 | | - .find(|&l| l.starts_with("search")) |
128 | | - .map(|l| l.trim_start_matches("search").trim()) |
129 | | - else { |
130 | | - return NoSearchEntrySnafu.fail(); |
131 | | - }; |
132 | | - |
133 | | - let Some(shortest_entry) = last_search_entry |
134 | | - .split_ascii_whitespace() |
135 | | - .min_by_key(|item| item.len()) |
136 | | - else { |
137 | | - return AmbiguousDomainEntriesSnafu.fail(); |
138 | | - }; |
139 | | - |
140 | | - // NOTE (@Techassi): This is really sad and bothers me more than I would like to admit. This |
141 | | - // clone could be removed by using the code directly in the calling function. But that would |
142 | | - // remove the possibility to easily test the parsing. |
143 | | - Ok(shortest_entry.to_owned()) |
| 47 | + }) |
144 | 48 | } |
145 | 49 |
|
146 | 50 | #[cfg(test)] |
147 | 51 | mod tests { |
148 | | - use std::path::PathBuf; |
149 | | - |
150 | 52 | use super::*; |
151 | | - use rstest::rstest; |
| 53 | + |
| 54 | + #[test] |
| 55 | + fn default_kubernetes_cluster_domain_value() { |
| 56 | + assert_eq!( |
| 57 | + retrieve_cluster_domain().unwrap().to_string(), |
| 58 | + "cluster.local" |
| 59 | + ); |
| 60 | + } |
152 | 61 |
|
153 | 62 | #[test] |
154 | 63 | fn use_different_kubernetes_cluster_domain_value() { |
155 | | - let cluster_domain = "my-cluster.local".to_string(); |
| 64 | + let cluster_domain = "my-cluster.local"; |
156 | 65 |
|
157 | | - // set different domain via env var |
| 66 | + // Set custom cluster domain via env var |
158 | 67 | unsafe { |
159 | | - env::set_var(KUBERNETES_CLUSTER_DOMAIN_ENV, &cluster_domain); |
| 68 | + env::set_var(KUBERNETES_CLUSTER_DOMAIN_ENV, cluster_domain); |
160 | 69 | } |
161 | 70 |
|
162 | | - // initialize the lock |
163 | | - let _ = KUBERNETES_CLUSTER_DOMAIN.set(retrieve_cluster_domain().unwrap()); |
164 | | - |
165 | 71 | assert_eq!( |
166 | | - cluster_domain, |
167 | | - KUBERNETES_CLUSTER_DOMAIN.get().unwrap().to_string() |
| 72 | + retrieve_cluster_domain().unwrap().to_string(), |
| 73 | + cluster_domain |
168 | 74 | ); |
169 | 75 | } |
170 | | - |
171 | | - #[rstest] |
172 | | - fn parse_resolv_conf_pass( |
173 | | - #[files("fixtures/cluster_domain/pass/*.resolv.conf")] path: PathBuf, |
174 | | - ) { |
175 | | - assert_eq!( |
176 | | - retrieve_cluster_domain_from_resolv_conf(path).unwrap(), |
177 | | - KUBERNETES_CLUSTER_DOMAIN_DEFAULT |
178 | | - ); |
179 | | - } |
180 | | - |
181 | | - #[rstest] |
182 | | - fn parse_resolv_conf_fail( |
183 | | - #[files("fixtures/cluster_domain/fail/*.resolv.conf")] path: PathBuf, |
184 | | - ) { |
185 | | - assert!(retrieve_cluster_domain_from_resolv_conf(path).is_err()); |
186 | | - } |
187 | 76 | } |
0 commit comments