datadog_agent_commons/ipc/
config.rs1use std::{path::PathBuf, time::Duration};
4
5use backon::{BackoffBuilder, ConstantBuilder};
6use saluki_config::GenericConfiguration;
7use saluki_error::{ErrorContext as _, GenericError};
8use serde::Deserialize;
9use tonic::transport::Uri;
10#[cfg(not(target_os = "linux"))]
11use tracing::warn;
12
13use crate::platform::PlatformSettings;
14
15fn default_agent_ipc_endpoint() -> Uri {
16 Uri::from_static("https://127.0.0.1:5001")
17}
18
19const fn default_connect_retry_attempts() -> usize {
20 10
21}
22
23const fn default_grpc_max_message_size() -> usize {
24 128 * 1024 * 1024
25}
26
27const fn default_connect_retry_backoff() -> Duration {
28 Duration::from_secs(2)
29}
30
31#[derive(Deserialize)]
33#[serde(default)]
34pub struct IpcAuthConfiguration {
35 auth_token_file_path: PathBuf,
42
43 ipc_cert_file_path: Option<PathBuf>,
52}
53
54impl IpcAuthConfiguration {
55 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
61 config
62 .as_typed::<Self>()
63 .error_context("Failed to parse Datadog Agent IPC authentication configuration.")
64 }
65
66 pub fn auth_token_file_path(&self) -> PathBuf {
68 if self.auth_token_file_path.as_os_str().is_empty() {
69 return PlatformSettings::get_auth_token_path();
70 }
71
72 self.auth_token_file_path.clone()
73 }
74
75 pub fn ipc_cert_file_path(&self) -> PathBuf {
77 if let Some(path) = self.ipc_cert_file_path.as_ref() {
79 if !path.as_os_str().is_empty() {
80 return path.clone();
81 }
82 }
83
84 let auth_token_dir = if self.auth_token_file_path.as_os_str().is_empty() {
86 PlatformSettings::get_config_dir_path()
87 } else {
88 self.auth_token_file_path
89 .parent()
90 .unwrap_or(PlatformSettings::get_config_dir_path())
91 };
92
93 auth_token_dir.join(PlatformSettings::get_ipc_cert_filename())
94 }
95}
96
97impl Default for IpcAuthConfiguration {
98 fn default() -> Self {
99 Self {
100 auth_token_file_path: PlatformSettings::get_auth_token_path(),
101 ipc_cert_file_path: None,
102 }
103 }
104}
105
106#[derive(Deserialize)]
108pub struct RemoteAgentClientConfiguration {
109 #[serde(
119 rename = "agent_ipc_endpoint",
120 with = "http_serde_ext::uri",
121 default = "default_agent_ipc_endpoint"
122 )]
123 ipc_endpoint: Uri,
124
125 cmd_port: Option<u16>,
129
130 #[serde(flatten, default)]
132 auth: IpcAuthConfiguration,
133
134 #[serde(default = "default_connect_retry_attempts")]
138 connect_retry_attempts: usize,
139
140 #[serde(default = "default_connect_retry_backoff")]
144 connect_retry_backoff: Duration,
145
146 #[serde(
150 rename = "agent_ipc_grpc_max_message_size",
151 default = "default_grpc_max_message_size"
152 )]
153 grpc_max_message_size: usize,
154
155 #[cfg(target_os = "linux")]
169 #[serde(default, deserialize_with = "deserialize_vsock_addr")]
170 vsock_addr: Option<u32>,
171
172 #[cfg(not(target_os = "linux"))]
174 #[serde(default)]
175 vsock_addr: String,
176}
177
178#[cfg(target_os = "linux")]
179fn deserialize_vsock_addr<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
180where
181 D: serde::Deserializer<'de>,
182{
183 use serde::de::Error as _;
184 match Option::<String>::deserialize(deserializer)?.as_deref() {
185 None | Some("") => Ok(None),
186 Some("host") => Ok(Some(2)), Some("hypervisor") => Ok(Some(0)), Some("local") => Ok(Some(3)), Some(other) => Err(D::Error::custom(format!(
190 "invalid vsock address '{}'; expected one of: host, hypervisor, local",
191 other
192 ))),
193 }
194}
195
196impl RemoteAgentClientConfiguration {
197 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
203 let this = config
204 .as_typed::<Self>()
205 .error_context("Failed to parse Datadog Agent IPC client configuration.")?;
206
207 #[cfg(not(target_os = "linux"))]
208 if !this.vsock_addr.is_empty() {
209 warn!("`vsock_addr` is configured but vsock is only supported on Linux. Setting will be ignored.");
210 }
211
212 Ok(this)
213 }
214
215 pub fn auth(&self) -> &IpcAuthConfiguration {
217 &self.auth
218 }
219
220 pub fn endpoint(&self) -> Result<Uri, GenericError> {
224 if let Some(cmd_port) = self.cmd_port {
225 format!("https://127.0.0.1:{}", cmd_port)
226 .parse::<Uri>()
227 .with_error_context(|| format!("failed to build URI from cmd_port {cmd_port}"))
228 } else {
229 Ok(self.ipc_endpoint.clone())
230 }
231 }
232
233 pub fn grpc_max_message_size(&self) -> usize {
235 self.grpc_max_message_size
236 }
237
238 #[cfg(target_os = "linux")]
247 pub fn vsock_addr(&self) -> Result<Option<tokio_vsock::VsockAddr>, GenericError> {
248 let Some(cid) = self.vsock_addr else {
249 return Ok(None);
250 };
251 let port = self
252 .endpoint()?
253 .port_u16()
254 .map(u32::from)
255 .ok_or_else(|| saluki_error::generic_error!("vsock requires an explicit port in the IPC endpoint"))?;
256 Ok(Some(tokio_vsock::VsockAddr::new(cid, port)))
257 }
258}
259
260impl BackoffBuilder for &RemoteAgentClientConfiguration {
261 type Backoff = <ConstantBuilder as BackoffBuilder>::Backoff;
262
263 fn build(self) -> Self::Backoff {
264 ConstantBuilder::default()
265 .with_delay(self.connect_retry_backoff)
266 .with_max_times(self.connect_retry_attempts)
267 .build()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use std::path::{Path, PathBuf};
274
275 use saluki_config::ConfigurationLoader;
276
277 use super::RemoteAgentClientConfiguration;
278 use crate::platform::PlatformSettings;
279
280 async fn get_remote_agent_config(
281 ipc_cert_file_path: Option<&Path>, auth_token_file_path: Option<&Path>,
282 ) -> RemoteAgentClientConfiguration {
283 let mut values = serde_json::Map::new();
285 if let Some(path) = ipc_cert_file_path {
286 values.insert(
287 "ipc_cert_file_path".to_string(),
288 path.to_string_lossy().into_owned().into(),
289 );
290 }
291 if let Some(path) = auth_token_file_path {
292 values.insert(
293 "auth_token_file_path".to_string(),
294 path.to_string_lossy().into_owned().into(),
295 );
296 }
297
298 let (base_config, _) =
299 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
300 RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
301 }
302
303 #[tokio::test]
304 async fn ipc_cert_file_path_empty_config() {
305 let default_auth_token_path = PlatformSettings::get_auth_token_path();
306
307 let config = get_remote_agent_config(None, None).await;
310 assert_eq!(
311 config.auth().ipc_cert_file_path().parent(),
312 default_auth_token_path.as_path().parent()
313 );
314 assert_eq!(
315 config.auth().ipc_cert_file_path().file_name().map(Path::new),
316 Some(PlatformSettings::get_ipc_cert_filename())
317 );
318 }
319
320 #[tokio::test]
321 async fn ipc_cert_file_path_defaults() {
322 let default_auth_token_path = PlatformSettings::get_auth_token_path();
323
324 let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
327 assert_eq!(
328 config.auth().ipc_cert_file_path().parent(),
329 default_auth_token_path.as_path().parent()
330 );
331 assert_eq!(
332 config.auth().ipc_cert_file_path().file_name().map(Path::new),
333 Some(PlatformSettings::get_ipc_cert_filename())
334 );
335 }
336
337 #[tokio::test]
338 async fn ipc_cert_file_path_explicitly_set() {
339 let default_auth_token_path = PlatformSettings::get_auth_token_path();
340 let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
341
342 let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
344 assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
345 }
346
347 #[tokio::test]
348 async fn ipc_cert_file_path_custom_auth_token_path() {
349 let custom_auth_token_path = PathBuf::from("/secret/auth_token");
350
351 let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
354 assert_eq!(
355 config.auth().ipc_cert_file_path().parent(),
356 custom_auth_token_path.as_path().parent()
357 );
358 assert_eq!(
359 config.auth().ipc_cert_file_path().file_name().map(Path::new),
360 Some(PlatformSettings::get_ipc_cert_filename())
361 );
362 }
363
364 #[tokio::test]
365 async fn ipc_cert_file_path_invalid_auth_token_path() {
366 let invalid_auth_token_path = PathBuf::from("/");
367
368 let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
371 assert_eq!(
372 config.auth().ipc_cert_file_path().parent(),
373 Some(PlatformSettings::get_config_dir_path())
374 );
375 assert_eq!(
376 config.auth().ipc_cert_file_path().file_name().map(Path::new),
377 Some(PlatformSettings::get_ipc_cert_filename())
378 );
379 }
380
381 async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
382 let (base_config, _) =
383 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
384 RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
385 }
386
387 #[tokio::test]
388 async fn endpoint_defaults_to_port_5001() {
389 let config = config_from_values(serde_json::Map::new()).await;
390 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
391 }
392
393 #[tokio::test]
394 async fn endpoint_uses_cmd_port() {
395 let mut values = serde_json::Map::new();
396 values.insert("cmd_port".to_string(), 7777.into());
397 let config = config_from_values(values).await;
398 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
399 }
400
401 #[tokio::test]
402 async fn cmd_port_takes_precedence_over_ipc_endpoint() {
403 let mut values = serde_json::Map::new();
404 values.insert("cmd_port".to_string(), 8888.into());
405 values.insert("agent_ipc_endpoint".to_string(), "https://10.0.0.1:3333".into());
406 let config = config_from_values(values).await;
407 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:8888/");
408 }
409
410 #[tokio::test]
411 async fn ipc_endpoint_used_when_no_cmd_port() {
412 let mut values = serde_json::Map::new();
413 values.insert("agent_ipc_endpoint".to_string(), "https://10.0.0.1:3333".into());
414 let config = config_from_values(values).await;
415 assert_eq!(config.endpoint().unwrap().to_string(), "https://10.0.0.1:3333/");
416 }
417
418 #[cfg(target_os = "linux")]
419 #[tokio::test]
420 async fn vsock_addr_valid_values() {
421 let cases: &[(&str, Option<u32>)] = &[
423 ("", None),
424 ("host", Some(2)),
425 ("hypervisor", Some(0)),
426 ("local", Some(3)),
427 ];
428
429 for (input, expected_cid) in cases {
430 let mut values = serde_json::Map::new();
431 values.insert("vsock_addr".to_string(), (*input).into());
432 values.insert("cmd_port".to_string(), 5001u16.into());
433 let config = config_from_values(values).await;
434 let result = config
435 .vsock_addr()
436 .expect("vsock_addr() should not error with cmd_port set");
437 assert_eq!(result.map(|a| a.cid()), *expected_cid, "input: {input:?}");
438 }
439 }
440
441 #[cfg(target_os = "linux")]
442 #[tokio::test]
443 async fn vsock_addr_invalid_values() {
444 let cases = &["invalid", "2", "HOST", "host ", "vm0"];
445
446 for input in cases {
447 let mut values = serde_json::Map::new();
448 values.insert("vsock_addr".to_string(), (*input).into());
449 let (base_config, _) =
450 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
451 assert!(
452 RemoteAgentClientConfiguration::from_configuration(&base_config).is_err(),
453 "expected error for input: {input:?}",
454 );
455 }
456 }
457}