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();
267 if let Some(path) = ipc_cert_file_path {
268 values.insert(
269 "ipc_cert_file_path".to_string(),
270 path.to_string_lossy().into_owned().into(),
271 );
272 }
273 if let Some(path) = auth_token_file_path {
274 values.insert(
275 "auth_token_file_path".to_string(),
276 path.to_string_lossy().into_owned().into(),
277 );
278 }
279
280 config_from_values(values).await
281 }
282
283 #[tokio::test]
284 async fn ipc_cert_file_path_empty_config() {
285 let default_auth_token_path = PlatformSettings::get_auth_token_path();
286
287 let config = get_remote_agent_config(None, None).await;
290 assert_eq!(
291 config.auth().ipc_cert_file_path().parent(),
292 default_auth_token_path.as_path().parent()
293 );
294 assert_eq!(
295 config.auth().ipc_cert_file_path().file_name().map(Path::new),
296 Some(PlatformSettings::get_ipc_cert_filename())
297 );
298 }
299
300 #[tokio::test]
301 async fn ipc_cert_file_path_defaults() {
302 let default_auth_token_path = PlatformSettings::get_auth_token_path();
303
304 let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
307 assert_eq!(
308 config.auth().ipc_cert_file_path().parent(),
309 default_auth_token_path.as_path().parent()
310 );
311 assert_eq!(
312 config.auth().ipc_cert_file_path().file_name().map(Path::new),
313 Some(PlatformSettings::get_ipc_cert_filename())
314 );
315 }
316
317 #[tokio::test]
318 async fn ipc_cert_file_path_explicitly_set() {
319 let default_auth_token_path = PlatformSettings::get_auth_token_path();
320 let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
321
322 let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
324 assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
325 }
326
327 #[tokio::test]
328 async fn ipc_cert_file_path_custom_auth_token_path() {
329 let custom_auth_token_path = PathBuf::from("/secret/auth_token");
330
331 let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
334 assert_eq!(
335 config.auth().ipc_cert_file_path().parent(),
336 custom_auth_token_path.as_path().parent()
337 );
338 assert_eq!(
339 config.auth().ipc_cert_file_path().file_name().map(Path::new),
340 Some(PlatformSettings::get_ipc_cert_filename())
341 );
342 }
343
344 #[tokio::test]
345 async fn ipc_cert_file_path_invalid_auth_token_path() {
346 let invalid_auth_token_path = PathBuf::from("/");
347
348 let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
351 assert_eq!(
352 config.auth().ipc_cert_file_path().parent(),
353 Some(PlatformSettings::get_config_dir_path())
354 );
355 assert_eq!(
356 config.auth().ipc_cert_file_path().file_name().map(Path::new),
357 Some(PlatformSettings::get_ipc_cert_filename())
358 );
359 }
360
361 async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
362 let (base_config, _) =
363 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
364 RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
365 }
366
367 #[tokio::test]
368 async fn endpoint_defaults_to_port_5001() {
369 let config = config_from_values(serde_json::Map::new()).await;
370 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
371 }
372
373 #[tokio::test]
374 async fn endpoint_uses_cmd_port() {
375 let mut values = serde_json::Map::new();
376 values.insert("cmd_port".to_string(), 7777.into());
377 let config = config_from_values(values).await;
378 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
379 }
380
381 #[cfg(target_os = "linux")]
382 #[tokio::test]
383 async fn vsock_addr_valid_values() {
384 let cases: &[(&str, Option<u32>)] = &[
386 ("", None),
387 ("host", Some(2)),
388 ("hypervisor", Some(0)),
389 ("local", Some(3)),
390 ];
391
392 for (input, expected_cid) in cases {
393 let mut values = serde_json::Map::new();
394 values.insert("vsock_addr".to_string(), (*input).into());
395 values.insert("cmd_port".to_string(), 5001u16.into());
396 let config = config_from_values(values).await;
397 let result = config
398 .vsock_addr()
399 .expect("vsock_addr() should not error with cmd_port set");
400 assert_eq!(result.map(|a| a.cid()), *expected_cid, "input: {input:?}");
401 }
402 }
403
404 #[cfg(target_os = "linux")]
405 #[tokio::test]
406 async fn vsock_addr_invalid_values() {
407 let cases = &["invalid", "2", "HOST", "host ", "vm0"];
408
409 for input in cases {
410 let mut values = serde_json::Map::new();
411 values.insert("vsock_addr".to_string(), (*input).into());
412 let (base_config, _) =
413 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
414 assert!(
415 RemoteAgentClientConfiguration::from_configuration(&base_config).is_err(),
416 "expected error for input: {input:?}",
417 );
418 }
419 }
420}