Skip to content

Commit 0269a4e

Browse files
committed
Merge remote-tracking branch 'origin/main' into spike/domain-in-client
2 parents 62b1bad + 60cb372 commit 0269a4e

File tree

4 files changed

+203
-0
lines changed

4 files changed

+203
-0
lines changed

crates/stackable-operator/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Fixed
8+
9+
- Fix Kubernetes cluster domain parsing from resolv.conf, e.g. on AWS EKS.
10+
We now only consider Kubernetes services domains instead of all domains (which could include non-Kubernetes domains) ([#895]).
11+
12+
[#895]: https://github.com/stackabletech/operator-rs/pull/895
13+
714
## [0.79.0] - 2024-10-18
815

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

0 commit comments

Comments
 (0)