saluki_components/config/
cluster_agent.rs1use std::net::IpAddr;
4
5#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct ClusterAgentConfiguration {
12 pub enabled: bool,
17
18 pub url: Option<String>,
24
25 pub auth_token: Option<String>,
30
31 pub kubernetes_service_name: String,
38}
39
40impl ClusterAgentConfiguration {
41 pub fn endpoint_and_token(&self) -> Option<(String, String)> {
46 self.endpoint_and_token_with_env(|key| std::env::var(key).ok())
47 }
48
49 fn endpoint_and_token_with_env<F>(&self, env_lookup: F) -> Option<(String, String)>
50 where
51 F: Fn(&str) -> Option<String>,
52 {
53 if !self.enabled {
54 return None;
55 }
56
57 let endpoint = self.resolve_endpoint(env_lookup)?;
58
59 Some((endpoint, self.auth_token.clone()?))
60 }
61
62 fn resolve_endpoint<F>(&self, env_lookup: F) -> Option<String>
63 where
64 F: Fn(&str) -> Option<String>,
65 {
66 if let Some(url) = self.url.as_deref() {
67 return normalize_cluster_agent_url(url);
68 }
69
70 if self.kubernetes_service_name.is_empty() {
71 return None;
72 }
73
74 resolve_kubernetes_service_endpoint(&self.kubernetes_service_name, env_lookup)
75 }
76}
77
78fn normalize_cluster_agent_url(url: &str) -> Option<String> {
79 let normalized = if url.contains("://") {
80 url.to_string()
81 } else {
82 format!("https://{url}")
83 };
84
85 let parsed = url::Url::parse(&normalized).ok()?;
86 if parsed.scheme() == "https" && parsed.host_str().is_some() {
87 Some(normalized)
88 } else {
89 None
90 }
91}
92
93fn resolve_kubernetes_service_endpoint<F>(service_name: &str, env_lookup: F) -> Option<String>
94where
95 F: Fn(&str) -> Option<String>,
96{
97 let env_prefix = service_name.to_uppercase().replace('-', "_");
98 let host_env = format!("{env_prefix}_SERVICE_HOST");
99 let port_env = format!("{env_prefix}_SERVICE_PORT");
100
101 let host = env_lookup(&host_env)?.trim().to_string();
102 let port = env_lookup(&port_env)?.trim().to_string();
103 if host.is_empty() || port.is_empty() {
104 return None;
105 }
106
107 normalize_cluster_agent_url(&join_host_port(&host, &port))
108}
109
110fn join_host_port(host: &str, port: &str) -> String {
111 match host.parse::<IpAddr>() {
112 Ok(IpAddr::V6(_)) => format!("[{host}]:{port}"),
113 _ => format!("{host}:{port}"),
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 const DEFAULT_SERVICE_NAME: &str = "datadog-cluster-agent";
123
124 fn enabled_config() -> ClusterAgentConfiguration {
125 ClusterAgentConfiguration {
126 enabled: true,
127 url: None,
128 auth_token: Some("secret-token".to_string()),
129 kubernetes_service_name: DEFAULT_SERVICE_NAME.to_string(),
130 }
131 }
132
133 #[test]
134 fn endpoint_and_token_requires_enabled_cluster_agent() {
135 let config = ClusterAgentConfiguration {
136 enabled: false,
137 url: Some("https://cluster-agent.example.com".to_string()),
138 ..enabled_config()
139 };
140
141 assert_eq!(config.endpoint_and_token_with_env(env_lookup(&[])), None);
142 }
143
144 #[test]
145 fn endpoint_and_token_requires_resolvable_endpoint() {
146 let config = enabled_config();
147
148 assert_eq!(config.endpoint_and_token_with_env(env_lookup(&[])), None);
149 }
150
151 #[test]
152 fn endpoint_and_token_requires_https_url() {
153 let config = ClusterAgentConfiguration {
154 url: Some("http://cluster-agent.example.com".to_string()),
155 ..enabled_config()
156 };
157
158 assert_eq!(config.endpoint_and_token_with_env(env_lookup(&[])), None);
159 }
160
161 #[test]
162 fn endpoint_and_token_adds_https_scheme_to_url() {
163 let config = ClusterAgentConfiguration {
164 url: Some("cluster-agent.example.com:5005".to_string()),
165 ..enabled_config()
166 };
167
168 assert_eq!(
169 config.endpoint_and_token_with_env(env_lookup(&[])),
170 Some((
171 "https://cluster-agent.example.com:5005".to_string(),
172 "secret-token".to_string()
173 ))
174 );
175 }
176
177 #[test]
178 fn endpoint_and_token_requires_a_token() {
179 let config = ClusterAgentConfiguration {
180 url: Some("https://cluster-agent.example.com".to_string()),
181 auth_token: None,
182 ..enabled_config()
183 };
184
185 assert_eq!(config.endpoint_and_token_with_env(env_lookup(&[])), None);
186 }
187
188 #[test]
189 fn endpoint_and_token_returns_https_url_and_token() {
190 let config = ClusterAgentConfiguration {
191 url: Some("https://cluster-agent.example.com".to_string()),
192 ..enabled_config()
193 };
194
195 assert_eq!(
196 config.endpoint_and_token_with_env(env_lookup(&[])),
197 Some((
198 "https://cluster-agent.example.com".to_string(),
199 "secret-token".to_string()
200 ))
201 );
202 }
203
204 #[test]
205 fn endpoint_and_token_resolves_default_kubernetes_service_env() {
206 let config = enabled_config();
207
208 assert_eq!(
209 config.endpoint_and_token_with_env(env_lookup(&[
210 ("DATADOG_CLUSTER_AGENT_SERVICE_HOST", "127.0.0.1"),
211 ("DATADOG_CLUSTER_AGENT_SERVICE_PORT", "443"),
212 ])),
213 Some(("https://127.0.0.1:443".to_string(), "secret-token".to_string()))
214 );
215 }
216
217 #[test]
218 fn endpoint_and_token_resolves_configured_kubernetes_service_env() {
219 let config = ClusterAgentConfiguration {
220 kubernetes_service_name: "custom-cluster-agent".to_string(),
221 ..enabled_config()
222 };
223
224 assert_eq!(
225 config.endpoint_and_token_with_env(env_lookup(&[
226 ("CUSTOM_CLUSTER_AGENT_SERVICE_HOST", "10.0.0.7"),
227 ("CUSTOM_CLUSTER_AGENT_SERVICE_PORT", "5005"),
228 ])),
229 Some(("https://10.0.0.7:5005".to_string(), "secret-token".to_string()))
230 );
231 }
232
233 #[test]
234 fn an_empty_kubernetes_service_name_turns_discovery_off() {
235 let config = ClusterAgentConfiguration {
238 kubernetes_service_name: String::new(),
239 ..enabled_config()
240 };
241
242 assert_eq!(
243 config.endpoint_and_token_with_env(env_lookup(&[
244 ("DATADOG_CLUSTER_AGENT_SERVICE_HOST", "127.0.0.1"),
245 ("DATADOG_CLUSTER_AGENT_SERVICE_PORT", "443"),
246 ])),
247 None
248 );
249 }
250
251 #[test]
252 fn endpoint_and_token_wraps_kubernetes_service_ipv6_host() {
253 let config = enabled_config();
254
255 assert_eq!(
256 config.endpoint_and_token_with_env(env_lookup(&[
257 ("DATADOG_CLUSTER_AGENT_SERVICE_HOST", "fd38:552b:2959::4f4a"),
258 ("DATADOG_CLUSTER_AGENT_SERVICE_PORT", "5005"),
259 ])),
260 Some((
261 "https://[fd38:552b:2959::4f4a]:5005".to_string(),
262 "secret-token".to_string()
263 ))
264 );
265 }
266
267 #[test]
268 fn endpoint_and_token_prefers_configured_url_over_kubernetes_service_env() {
269 let config = ClusterAgentConfiguration {
270 url: Some("https://configured-cluster-agent.example.com".to_string()),
271 kubernetes_service_name: "custom-cluster-agent".to_string(),
272 ..enabled_config()
273 };
274
275 assert_eq!(
276 config.endpoint_and_token_with_env(env_lookup(&[
277 ("CUSTOM_CLUSTER_AGENT_SERVICE_HOST", "10.0.0.7"),
278 ("CUSTOM_CLUSTER_AGENT_SERVICE_PORT", "5005"),
279 ])),
280 Some((
281 "https://configured-cluster-agent.example.com".to_string(),
282 "secret-token".to_string()
283 ))
284 );
285 }
286
287 fn env_lookup<'a>(entries: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
288 move |key| {
289 entries
290 .iter()
291 .find_map(|(entry_key, entry_value)| (*entry_key == key).then(|| (*entry_value).to_string()))
292 }
293 }
294}