Skip to content

Commit 32e41d8

Browse files
refactor: Adjust cluster domain parsing code
There is still some bits and pieces we want to change, mostly regarding the error type / error handling. This commit also moves the input fixtures for testing the resolv parser into dedicated files and utilizies rstest's #[files] attribute to generate tests based on these files. Co-authored-by: Nick Larsen <[email protected]>
1 parent 53d8c9f commit 32e41d8

File tree

7 files changed

+87
-171
lines changed

7 files changed

+87
-171
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ repos:
5151
name: .scripts/verify-crate-versions
5252
language: system
5353
entry: .scripts/verify_crate_versions.sh
54-
stages: [commit, merge-commit, manual]
54+
stages: [pre-commit, pre-merge-commit, manual]
5555
pass_filenames: false
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
nameserver 10.243.21.53
2+
options ndots:5
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
search baz svc.foo.bar foo.bar
2+
search sble-operators.svc.cluster.local svc.cluster.local cluster.local
3+
nameserver 10.243.21.53
4+
options ndots:5
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
search sble-operators.svc.cluster.local svc.cluster.local cluster.local
2+
nameserver 10.243.21.53
3+
options ndots:5
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
search openshift-service-ca-operator.svc.cluster.local svc.cluster.local cluster.local cmx.repl-openshift.build
2+
nameserver 172.30.0.10
3+
options ndots:5

crates/stackable-operator/src/client.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use crate::kvp::LabelSelectorExt;
2-
use crate::utils::cluster_domain::{
3-
self, resolve_kubernetes_cluster_domain, KUBERNETES_CLUSTER_DOMAIN,
4-
};
2+
use crate::utils::cluster_domain::{self, retrieve_cluster_domain, KUBERNETES_CLUSTER_DOMAIN};
53

64
use either::Either;
75
use futures::StreamExt;
@@ -630,7 +628,7 @@ where
630628

631629
pub async fn initialize_operator(field_manager: Option<String>) -> Result<Client> {
632630
let _ = KUBERNETES_CLUSTER_DOMAIN
633-
.set(resolve_kubernetes_cluster_domain().context(ResolveKubernetesClusterDomainSnafu)?);
631+
.set(retrieve_cluster_domain().context(ResolveKubernetesClusterDomainSnafu)?);
634632
create_client(field_manager).await
635633
}
636634

Lines changed: 72 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,30 @@
1-
use std::{
2-
env,
3-
io::{self, BufRead},
4-
path::Path,
5-
sync::OnceLock,
6-
};
1+
use std::{env, path::Path, str::FromStr, sync::OnceLock};
72

83
use snafu::{OptionExt, ResultExt, Snafu};
94

105
use crate::commons::networking::DomainName;
116

12-
// Env vars
137
const KUBERNETES_CLUSTER_DOMAIN_ENV: &str = "KUBERNETES_CLUSTER_DOMAIN";
148
const KUBERNETES_SERVICE_HOST_ENV: &str = "KUBERNETES_SERVICE_HOST";
15-
// Misc
9+
1610
const KUBERNETES_CLUSTER_DOMAIN_DEFAULT: &str = "cluster.local";
1711
const RESOLVE_CONF_FILE_PATH: &str = "/etc/resolv.conf";
1812

13+
// TODO (@Techassi): Do we even need this many variants? Can we get rid of a bunch of variants and
14+
// fall back to defaults instead? Also trace the errors
1915
#[derive(Debug, Snafu)]
2016
pub enum Error {
21-
#[snafu(display("Env var '{name}' does not exist."))]
22-
EnvVarNotFound { source: env::VarError, name: String },
23-
24-
#[snafu(display("Could not find '{resolve_conf_file_path}'."))]
25-
ResolvConfNotFound {
26-
source: io::Error,
27-
resolve_conf_file_path: String,
28-
},
17+
#[snafu(display("failed to read resolv.conf"))]
18+
ReadResolvConfFile { source: std::io::Error },
2919

30-
#[snafu(display("The provided cluster domain '{cluster_domain}' is not valid."))]
31-
InvalidDomain {
20+
#[snafu(display("failed to parse {cluster_domain:?} as cluster domain"))]
21+
ParseDomainName {
3222
source: crate::validation::Errors,
3323
cluster_domain: String,
3424
},
3525

36-
#[snafu(display("No 'search' entries found in '{RESOLVE_CONF_FILE_PATH}'."))]
37-
SearchEntryNotFound { resolve_conf_file_path: String },
26+
#[snafu(display("No 'search' entries found in"))]
27+
SearchEntryNotFound,
3828

3929
#[snafu(display("Could not trim search entry in '{search_entry_line}'."))]
4030
TrimSearchEntryFailed { search_entry_line: String },
@@ -73,152 +63,75 @@ pub enum Error {
7363
/// ```
7464
pub static KUBERNETES_CLUSTER_DOMAIN: OnceLock<DomainName> = OnceLock::new();
7565

76-
pub(crate) fn resolve_kubernetes_cluster_domain() -> Result<DomainName, Error> {
66+
pub(crate) fn retrieve_cluster_domain() -> Result<DomainName, Error> {
7767
// 1. Read KUBERNETES_CLUSTER_DOMAIN env var
7868
tracing::info!("Trying to determine the Kubernetes cluster domain...");
79-
match read_env_var(KUBERNETES_CLUSTER_DOMAIN_ENV) {
80-
Ok(cluster_domain) => {
69+
70+
match env::var(KUBERNETES_CLUSTER_DOMAIN_ENV) {
71+
Ok(cluster_domain) if !cluster_domain.is_empty() => {
8172
tracing::info!("Using Kubernetes cluster domain: '{cluster_domain}'");
82-
return cluster_domain
83-
.clone()
84-
.try_into()
85-
.context(InvalidDomainSnafu { cluster_domain });
73+
return DomainName::from_str(&cluster_domain)
74+
.context(ParseDomainNameSnafu { cluster_domain });
8675
}
87-
Err(_) => {
88-
tracing::info!("The env var '{KUBERNETES_CLUSTER_DOMAIN_ENV}' is not set.");
76+
_ => {
77+
tracing::info!("The env var '{KUBERNETES_CLUSTER_DOMAIN_ENV}' is not set or empty");
8978
}
9079
};
9180

92-
// 2. If no env var is set, check if we run in a clusterized (Kubernetes/Openshift) enviroment
81+
// 2. If no env var is set, check if we run in a clustered (Kubernetes/Openshift) environment
9382
// by checking if KUBERNETES_SERVICE_HOST is set: If not default to 'cluster.local'.
9483
tracing::info!("Trying to determine the operator runtime environment...");
95-
if read_env_var(KUBERNETES_SERVICE_HOST_ENV).is_err() {
96-
tracing::info!("The env var '{KUBERNETES_SERVICE_HOST_ENV}' is not set. This means we do not run in Kubernetes / Openshift. Defaulting cluster domain to '{KUBERNETES_CLUSTER_DOMAIN_DEFAULT}'.");
97-
return KUBERNETES_CLUSTER_DOMAIN_DEFAULT
98-
.to_string()
99-
.try_into()
100-
.context(InvalidDomainSnafu {
101-
cluster_domain: KUBERNETES_CLUSTER_DOMAIN_DEFAULT.to_string(),
102-
});
103-
}
10484

105-
// 3. Read and parse 'resolv.conf'. We are looking for the last "search" entry and filter for the shortest
106-
// element in that search line
107-
tracing::info!(
108-
"Running in clusterized environment. Attempting to parse '{RESOLVE_CONF_FILE_PATH}'..."
109-
);
110-
let resolve_conf_lines =
111-
read_file_from_path(RESOLVE_CONF_FILE_PATH).context(ResolvConfNotFoundSnafu {
112-
resolve_conf_file_path: RESOLVE_CONF_FILE_PATH.to_string(),
113-
})?;
85+
match env::var(KUBERNETES_SERVICE_HOST_ENV) {
86+
Ok(_) => {
87+
let cluster_domain = retrieve_cluster_domain_from_resolv_conf(RESOLVE_CONF_FILE_PATH)?;
11488

115-
let cluster_domain = parse_resolve_config(resolve_conf_lines)?;
116-
tracing::info!("Using Kubernetes cluster domain: '{cluster_domain}'");
89+
tracing::info!(
90+
cluster_domain,
91+
"Using Kubernetes cluster domain from {RESOLVE_CONF_FILE_PATH} file"
92+
);
11793

118-
cluster_domain
119-
.clone()
120-
.try_into()
121-
.context(InvalidDomainSnafu { cluster_domain })
94+
DomainName::from_str(&cluster_domain).context(ParseDomainNameSnafu { cluster_domain })
95+
}
96+
Err(_) => {
97+
tracing::info!(
98+
cluster_domain = KUBERNETES_CLUSTER_DOMAIN_DEFAULT,
99+
"Using default Kubernetes cluster domain"
100+
);
101+
Ok(DomainName::from_str(KUBERNETES_CLUSTER_DOMAIN_DEFAULT).expect("stuff"))
102+
}
103+
}
122104
}
123105

124-
/// Extract the Kubernetes cluster domain from the vectorized 'resolv.conf'.
125-
/// This will:
126-
/// 1. Use the last entry containing a 'search' prefix.
127-
/// 2. Strip 'search' from the last entry.
128-
/// 3. Return the shortest itme (e.g. 'cluster.local') in the whitespace seperated list.
129-
fn parse_resolve_config(resolv_conf: Vec<String>) -> Result<String, Error> {
130-
tracing::debug!(
131-
"Start parsing '{RESOLVE_CONF_FILE_PATH}' to retrieve the Kubernetes cluster domain..."
132-
);
133-
134-
let last_search_entry =
135-
find_last_search_entry(&resolv_conf).context(SearchEntryNotFoundSnafu {
136-
resolve_conf_file_path: RESOLVE_CONF_FILE_PATH.to_string(),
137-
})?;
138-
139-
let last_search_entry_content =
140-
trim_search_line(&last_search_entry).context(TrimSearchEntryFailedSnafu {
141-
search_entry_line: last_search_entry.to_string(),
142-
})?;
143-
144-
let shortest_search_entry = find_shortest_entry(last_search_entry_content)
106+
fn retrieve_cluster_domain_from_resolv_conf<P>(path: P) -> Result<String, Error>
107+
where
108+
P: AsRef<Path>,
109+
{
110+
let content = std::fs::read_to_string(path).context(ReadResolvConfFileSnafu)?;
111+
112+
let last = content
113+
.lines()
114+
.map(|l| l.trim())
115+
.filter(|l| l.starts_with("search"))
116+
.map(|l| l.trim_start_matches("search"))
117+
.last()
118+
.context(SearchEntryNotFoundSnafu)?;
119+
120+
let shortest_entry = last
121+
.split_ascii_whitespace()
122+
.min_by_key(|item| item.len())
145123
.context(LookupClusterDomainEntryFailedSnafu)?;
146124

147-
Ok(shortest_search_entry.into())
148-
}
149-
150-
/// Read an ENV variable
151-
fn read_env_var(name: &str) -> Result<String, Error> {
152-
env::var(name).context(EnvVarNotFoundSnafu { name })
153-
}
154-
155-
// Function to read the contents of a file and return all lines as Vec<String>
156-
fn read_file_from_path(resolv_conf_file_path: &str) -> Result<Vec<String>, io::Error> {
157-
let file = std::fs::File::open(Path::new(resolv_conf_file_path))?;
158-
let reader = io::BufReader::new(file);
159-
160-
reader.lines().collect()
161-
}
162-
163-
/// Search the last entry containing the 'search' prefix. We are only interested in
164-
/// the last line (in case there are multiple entries which would be ignored by external tools).
165-
fn find_last_search_entry(lines: &[String]) -> Option<String> {
166-
lines
167-
.iter()
168-
.rev() // Start from the end to find the last occurrence
169-
.find(|line| line.trim().starts_with("search"))
170-
.cloned()
171-
}
172-
173-
/// Extract the content of the 'search' line. Basically stripping the 'search' prefix from the line like:
174-
/// 'search sble-operators.svc.cluster.local svc.cluster.local cluster.local' will become
175-
/// 'sble-operators.svc.cluster.local svc.cluster.local cluster.local'
176-
fn trim_search_line(search_line: &str) -> Option<&str> {
177-
search_line.trim().strip_prefix("search")
178-
}
179-
180-
/// Extract the shortest entry from a whitespace seperated string like:
181-
/// 'sble-operators.svc.cluster.local svc.cluster.local cluster.local'
182-
/// This will be 'cluster.local' here.
183-
fn find_shortest_entry(search_content: &str) -> Option<&str> {
184-
search_content
185-
.split_whitespace()
186-
.min_by_key(|entry| entry.len())
125+
// NOTE (@Techassi): This is really sad and bothers me more than I would like to admit
126+
Ok(shortest_entry.to_owned())
187127
}
188128

189129
#[cfg(test)]
190130
mod tests {
191-
use super::*;
192-
193-
const KUBERNETES_RESOLV_CONF: &str = r#"""
194-
search sble-operators.svc.cluster.local svc.cluster.local cluster.local
195-
nameserver 10.243.21.53
196-
options ndots:5
197-
"""#;
198-
199-
const OPENSHIFT_RESOLV_CONF: &str = r#"""
200-
search openshift-service-ca-operator.svc.cluster.local svc.cluster.local cluster.local cmx.repl-openshift.build
201-
nameserver 172.30.0.10
202-
options ndots:5
203-
"""#;
204-
205-
const KUBERNETES_RESOLV_CONF_MULTIPLE_SEARCH_ENTRIES: &str = r#"""
206-
search baz svc.foo.bar foo.bar
207-
search sble-operators.svc.cluster.local svc.cluster.local cluster.local
208-
nameserver 10.243.21.53
209-
options ndots:5
210-
"""#;
131+
use std::path::PathBuf;
211132

212-
const KUBERNETES_RESOLV_CONF_MISSING_SEARCH_ENTRIES: &str = r#"""
213-
nameserver 10.243.21.53
214-
options ndots:5
215-
"""#;
216-
217-
// Helper method to read resolv.conf from a string and not from file.
218-
fn read_file_from_string(contents: &str) -> Vec<String> {
219-
// Split the string by lines and collect into a Vec<String>
220-
contents.lines().map(|line| line.to_string()).collect()
221-
}
133+
use super::*;
134+
use rstest::rstest;
222135

223136
#[test]
224137
fn use_different_kubernetes_cluster_domain_value() {
@@ -230,35 +143,28 @@ mod tests {
230143
}
231144

232145
// initialize the lock
233-
let _ = KUBERNETES_CLUSTER_DOMAIN.set(resolve_kubernetes_cluster_domain().unwrap());
146+
let _ = KUBERNETES_CLUSTER_DOMAIN.set(retrieve_cluster_domain().unwrap());
234147

235148
assert_eq!(
236149
cluster_domain,
237150
KUBERNETES_CLUSTER_DOMAIN.get().unwrap().to_string()
238151
);
239152
}
240153

241-
#[test]
242-
fn parse_resolv_conf_success() {
243-
let correct_resolv_configs = vec![
244-
KUBERNETES_RESOLV_CONF,
245-
OPENSHIFT_RESOLV_CONF,
246-
KUBERNETES_RESOLV_CONF_MULTIPLE_SEARCH_ENTRIES,
247-
];
248-
249-
for resolv_conf in correct_resolv_configs {
250-
let lines = read_file_from_string(resolv_conf);
251-
let last_search_entry = find_last_search_entry(lines.as_slice()).unwrap();
252-
let search_entry = trim_search_line(&last_search_entry).unwrap();
253-
let cluster_domain = find_shortest_entry(search_entry).unwrap();
254-
assert_eq!(cluster_domain, KUBERNETES_CLUSTER_DOMAIN_DEFAULT);
255-
}
154+
#[rstest]
155+
fn parse_resolv_conf_pass(
156+
#[files("fixtures/cluster_domain/pass/*.resolv.conf")] path: PathBuf,
157+
) {
158+
assert_eq!(
159+
retrieve_cluster_domain_from_resolv_conf(path).unwrap(),
160+
KUBERNETES_CLUSTER_DOMAIN_DEFAULT
161+
);
256162
}
257163

258-
#[test]
259-
fn parse_resolv_conf_error_no_search_entry() {
260-
let lines = read_file_from_string(KUBERNETES_RESOLV_CONF_MISSING_SEARCH_ENTRIES);
261-
let last_search_entry = find_last_search_entry(lines.as_slice());
262-
assert_eq!(last_search_entry, None);
164+
#[rstest]
165+
fn parse_resolv_conf_fail(
166+
#[files("fixtures/cluster_domain/fail/*.resolv.conf")] path: PathBuf,
167+
) {
168+
assert!(retrieve_cluster_domain_from_resolv_conf(path).is_err());
263169
}
264170
}

0 commit comments

Comments
 (0)