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
15const DEFAULT_CMD_PORT: u16 = 5001;
16
17const fn default_cmd_port() -> u16 {
18 DEFAULT_CMD_PORT
19}
20
21const fn default_connect_retry_attempts() -> usize {
22 10
23}
24
25const fn default_grpc_max_message_size() -> usize {
26 128 * 1024 * 1024
27}
28
29const fn default_connect_retry_backoff() -> Duration {
30 Duration::from_secs(2)
31}
32
33#[derive(Deserialize)]
35#[serde(default)]
36pub struct IpcAuthConfiguration {
37 auth_token_file_path: PathBuf,
44
45 ipc_cert_file_path: Option<PathBuf>,
54}
55
56impl IpcAuthConfiguration {
57 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
63 config
64 .as_typed::<Self>()
65 .error_context("Failed to parse Datadog Agent IPC authentication configuration.")
66 }
67
68 pub fn auth_token_file_path(&self) -> PathBuf {
70 if self.auth_token_file_path.as_os_str().is_empty() {
71 return PlatformSettings::get_auth_token_path();
72 }
73
74 self.auth_token_file_path.clone()
75 }
76
77 pub fn ipc_cert_file_path(&self) -> PathBuf {
79 if let Some(path) = self.ipc_cert_file_path.as_ref() {
81 if !path.as_os_str().is_empty() {
82 return path.clone();
83 }
84 }
85
86 let auth_token_dir = if self.auth_token_file_path.as_os_str().is_empty() {
88 PlatformSettings::get_config_dir_path()
89 } else {
90 self.auth_token_file_path
91 .parent()
92 .unwrap_or(PlatformSettings::get_config_dir_path())
93 };
94
95 auth_token_dir.join(PlatformSettings::get_ipc_cert_filename())
96 }
97}
98
99impl Default for IpcAuthConfiguration {
100 fn default() -> Self {
101 Self {
102 auth_token_file_path: PlatformSettings::get_auth_token_path(),
103 ipc_cert_file_path: None,
104 }
105 }
106}
107
108#[derive(Deserialize)]
110pub struct RemoteAgentClientConfiguration {
111 #[serde(default = "default_cmd_port")]
115 cmd_port: u16,
116
117 #[serde(flatten, default)]
119 auth: IpcAuthConfiguration,
120
121 #[serde(default = "default_connect_retry_attempts")]
125 connect_retry_attempts: usize,
126
127 #[serde(default = "default_connect_retry_backoff")]
131 connect_retry_backoff: Duration,
132
133 #[serde(
137 rename = "agent_ipc_grpc_max_message_size",
138 default = "default_grpc_max_message_size"
139 )]
140 grpc_max_message_size: usize,
141
142 #[cfg(target_os = "linux")]
156 #[serde(default, deserialize_with = "deserialize_vsock_addr")]
157 vsock_addr: Option<u32>,
158
159 #[cfg(not(target_os = "linux"))]
161 #[serde(default)]
162 vsock_addr: String,
163}
164
165#[cfg(target_os = "linux")]
166fn deserialize_vsock_addr<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
167where
168 D: serde::Deserializer<'de>,
169{
170 use serde::de::Error as _;
171 match Option::<String>::deserialize(deserializer)?.as_deref() {
172 None | Some("") => Ok(None),
173 Some("host") => Ok(Some(2)), Some("hypervisor") => Ok(Some(0)), Some("local") => Ok(Some(3)), Some(other) => Err(D::Error::custom(format!(
177 "invalid vsock address '{}'; expected one of: host, hypervisor, local",
178 other
179 ))),
180 }
181}
182
183impl RemoteAgentClientConfiguration {
184 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
190 let this = config
191 .as_typed::<Self>()
192 .error_context("Failed to parse Datadog Agent IPC client configuration.")?;
193
194 #[cfg(not(target_os = "linux"))]
195 if !this.vsock_addr.is_empty() {
196 warn!("`vsock_addr` is configured but vsock is only supported on Linux. Setting will be ignored.");
197 }
198
199 Ok(this)
200 }
201
202 pub fn auth(&self) -> &IpcAuthConfiguration {
204 &self.auth
205 }
206
207 pub fn endpoint(&self) -> Result<Uri, GenericError> {
209 format!("https://127.0.0.1:{}", self.cmd_port)
210 .parse::<Uri>()
211 .with_error_context(|| format!("failed to build URI from cmd_port {}", self.cmd_port))
212 }
213
214 pub fn grpc_max_message_size(&self) -> usize {
216 self.grpc_max_message_size
217 }
218
219 #[cfg(target_os = "linux")]
228 pub fn vsock_addr(&self) -> Result<Option<tokio_vsock::VsockAddr>, GenericError> {
229 let Some(cid) = self.vsock_addr else {
230 return Ok(None);
231 };
232 let port = self
233 .endpoint()?
234 .port_u16()
235 .map(u32::from)
236 .ok_or_else(|| saluki_error::generic_error!("vsock requires an explicit port in the IPC endpoint"))?;
237 Ok(Some(tokio_vsock::VsockAddr::new(cid, port)))
238 }
239}
240
241impl BackoffBuilder for &RemoteAgentClientConfiguration {
242 type Backoff = <ConstantBuilder as BackoffBuilder>::Backoff;
243
244 fn build(self) -> Self::Backoff {
245 ConstantBuilder::default()
246 .with_delay(self.connect_retry_backoff)
247 .with_max_times(self.connect_retry_attempts)
248 .build()
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use std::path::{Path, PathBuf};
255
256 use saluki_config::ConfigurationLoader;
257
258 use super::RemoteAgentClientConfiguration;
259 use crate::platform::PlatformSettings;
260
261 async fn get_remote_agent_config(
262 ipc_cert_file_path: Option<&Path>, auth_token_file_path: Option<&Path>,
263 ) -> RemoteAgentClientConfiguration {
264 let mut values = serde_json::Map::new();
266 if let Some(path) = ipc_cert_file_path {
267 values.insert(
268 "ipc_cert_file_path".to_string(),
269 path.to_string_lossy().into_owned().into(),
270 );
271 }
272 if let Some(path) = auth_token_file_path {
273 values.insert(
274 "auth_token_file_path".to_string(),
275 path.to_string_lossy().into_owned().into(),
276 );
277 }
278
279 let (base_config, _) =
280 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
281 RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
282 }
283
284 #[tokio::test]
285 async fn ipc_cert_file_path_empty_config() {
286 let default_auth_token_path = PlatformSettings::get_auth_token_path();
287
288 let config = get_remote_agent_config(None, None).await;
291 assert_eq!(
292 config.auth().ipc_cert_file_path().parent(),
293 default_auth_token_path.as_path().parent()
294 );
295 assert_eq!(
296 config.auth().ipc_cert_file_path().file_name().map(Path::new),
297 Some(PlatformSettings::get_ipc_cert_filename())
298 );
299 }
300
301 #[tokio::test]
302 async fn ipc_cert_file_path_defaults() {
303 let default_auth_token_path = PlatformSettings::get_auth_token_path();
304
305 let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
308 assert_eq!(
309 config.auth().ipc_cert_file_path().parent(),
310 default_auth_token_path.as_path().parent()
311 );
312 assert_eq!(
313 config.auth().ipc_cert_file_path().file_name().map(Path::new),
314 Some(PlatformSettings::get_ipc_cert_filename())
315 );
316 }
317
318 #[tokio::test]
319 async fn ipc_cert_file_path_explicitly_set() {
320 let default_auth_token_path = PlatformSettings::get_auth_token_path();
321 let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
322
323 let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
325 assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
326 }
327
328 #[tokio::test]
329 async fn ipc_cert_file_path_custom_auth_token_path() {
330 let custom_auth_token_path = PathBuf::from("/secret/auth_token");
331
332 let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
335 assert_eq!(
336 config.auth().ipc_cert_file_path().parent(),
337 custom_auth_token_path.as_path().parent()
338 );
339 assert_eq!(
340 config.auth().ipc_cert_file_path().file_name().map(Path::new),
341 Some(PlatformSettings::get_ipc_cert_filename())
342 );
343 }
344
345 #[tokio::test]
346 async fn ipc_cert_file_path_invalid_auth_token_path() {
347 let invalid_auth_token_path = PathBuf::from("/");
348
349 let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
352 assert_eq!(
353 config.auth().ipc_cert_file_path().parent(),
354 Some(PlatformSettings::get_config_dir_path())
355 );
356 assert_eq!(
357 config.auth().ipc_cert_file_path().file_name().map(Path::new),
358 Some(PlatformSettings::get_ipc_cert_filename())
359 );
360 }
361
362 async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
363 let (base_config, _) =
364 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
365 RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
366 }
367
368 #[tokio::test]
369 async fn endpoint_defaults_to_port_5001() {
370 let config = config_from_values(serde_json::Map::new()).await;
371 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
372 }
373
374 #[tokio::test]
375 async fn endpoint_uses_cmd_port() {
376 let mut values = serde_json::Map::new();
377 values.insert("cmd_port".to_string(), 7777.into());
378 let config = config_from_values(values).await;
379 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
380 }
381
382 #[cfg(target_os = "linux")]
383 #[tokio::test]
384 async fn vsock_addr_valid_values() {
385 let cases: &[(&str, Option<u32>)] = &[
387 ("", None),
388 ("host", Some(2)),
389 ("hypervisor", Some(0)),
390 ("local", Some(3)),
391 ];
392
393 for (input, expected_cid) in cases {
394 let mut values = serde_json::Map::new();
395 values.insert("vsock_addr".to_string(), (*input).into());
396 values.insert("cmd_port".to_string(), 5001u16.into());
397 let config = config_from_values(values).await;
398 let result = config
399 .vsock_addr()
400 .expect("vsock_addr() should not error with cmd_port set");
401 assert_eq!(result.map(|a| a.cid()), *expected_cid, "input: {input:?}");
402 }
403 }
404
405 #[cfg(target_os = "linux")]
406 #[tokio::test]
407 async fn vsock_addr_invalid_values() {
408 let cases = &["invalid", "2", "HOST", "host ", "vm0"];
409
410 for input in cases {
411 let mut values = serde_json::Map::new();
412 values.insert("vsock_addr".to_string(), (*input).into());
413 let (base_config, _) =
414 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
415 assert!(
416 RemoteAgentClientConfiguration::from_configuration(&base_config).is_err(),
417 "expected error for input: {input:?}",
418 );
419 }
420 }
421}