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
25#[derive(Clone, Debug, Deserialize)]
27struct AgentIpcConfiguration {
28 #[serde(default = "default_grpc_max_message_size")]
32 grpc_max_message_size: usize,
33}
34
35impl Default for AgentIpcConfiguration {
36 fn default() -> Self {
37 Self {
38 grpc_max_message_size: default_grpc_max_message_size(),
39 }
40 }
41}
42
43const fn default_grpc_max_message_size() -> usize {
44 128 * 1024 * 1024
45}
46
47const fn default_connect_retry_backoff() -> Duration {
48 Duration::from_secs(2)
49}
50
51#[derive(Deserialize)]
53#[serde(default)]
54pub struct IpcAuthConfiguration {
55 auth_token_file_path: PathBuf,
62
63 ipc_cert_file_path: Option<PathBuf>,
72}
73
74impl IpcAuthConfiguration {
75 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
81 config
82 .as_typed::<Self>()
83 .error_context("Failed to parse Datadog Agent IPC authentication configuration.")
84 }
85
86 pub fn auth_token_file_path(&self) -> PathBuf {
88 if self.auth_token_file_path.as_os_str().is_empty() {
89 return PlatformSettings::get_auth_token_path();
90 }
91
92 self.auth_token_file_path.clone()
93 }
94
95 pub fn ipc_cert_file_path(&self) -> PathBuf {
97 if let Some(path) = self.ipc_cert_file_path.as_ref() {
99 if !path.as_os_str().is_empty() {
100 return path.clone();
101 }
102 }
103
104 let auth_token_dir = if self.auth_token_file_path.as_os_str().is_empty() {
106 PlatformSettings::get_config_dir_path()
107 } else {
108 self.auth_token_file_path
109 .parent()
110 .unwrap_or(PlatformSettings::get_config_dir_path())
111 };
112
113 auth_token_dir.join(PlatformSettings::get_ipc_cert_filename())
114 }
115}
116
117impl Default for IpcAuthConfiguration {
118 fn default() -> Self {
119 Self {
120 auth_token_file_path: PlatformSettings::get_auth_token_path(),
121 ipc_cert_file_path: None,
122 }
123 }
124}
125
126#[derive(Deserialize)]
128pub struct RemoteAgentClientConfiguration {
129 #[serde(default = "default_cmd_port")]
133 cmd_port: u16,
134
135 #[serde(flatten, default)]
137 auth: IpcAuthConfiguration,
138
139 #[serde(default = "default_connect_retry_attempts")]
143 connect_retry_attempts: usize,
144
145 #[serde(default = "default_connect_retry_backoff")]
149 connect_retry_backoff: Duration,
150
151 #[serde(default)]
153 agent_ipc: AgentIpcConfiguration,
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> {
222 format!("https://127.0.0.1:{}", self.cmd_port)
223 .parse::<Uri>()
224 .with_error_context(|| format!("failed to build URI from cmd_port {}", self.cmd_port))
225 }
226
227 pub fn grpc_max_message_size(&self) -> usize {
229 self.agent_ipc.grpc_max_message_size
230 }
231
232 #[cfg(target_os = "linux")]
241 pub fn vsock_addr(&self) -> Result<Option<tokio_vsock::VsockAddr>, GenericError> {
242 let Some(cid) = self.vsock_addr else {
243 return Ok(None);
244 };
245 let port = self
246 .endpoint()?
247 .port_u16()
248 .map(u32::from)
249 .ok_or_else(|| saluki_error::generic_error!("vsock requires an explicit port in the IPC endpoint"))?;
250 Ok(Some(tokio_vsock::VsockAddr::new(cid, port)))
251 }
252}
253
254impl BackoffBuilder for &RemoteAgentClientConfiguration {
255 type Backoff = <ConstantBuilder as BackoffBuilder>::Backoff;
256
257 fn build(self) -> Self::Backoff {
258 ConstantBuilder::default()
259 .with_delay(self.connect_retry_backoff)
260 .with_max_times(self.connect_retry_attempts)
261 .build()
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use std::path::{Path, PathBuf};
268
269 use saluki_config::ConfigurationLoader;
270
271 use super::RemoteAgentClientConfiguration;
272 use crate::platform::PlatformSettings;
273
274 async fn get_remote_agent_config(
275 ipc_cert_file_path: Option<&Path>, auth_token_file_path: Option<&Path>,
276 ) -> RemoteAgentClientConfiguration {
277 let mut values = serde_json::Map::new();
280 if let Some(path) = ipc_cert_file_path {
281 values.insert(
282 "ipc_cert_file_path".to_string(),
283 path.to_string_lossy().into_owned().into(),
284 );
285 }
286 if let Some(path) = auth_token_file_path {
287 values.insert(
288 "auth_token_file_path".to_string(),
289 path.to_string_lossy().into_owned().into(),
290 );
291 }
292
293 config_from_values(values).await
294 }
295
296 #[tokio::test]
297 async fn ipc_cert_file_path_empty_config() {
298 let default_auth_token_path = PlatformSettings::get_auth_token_path();
299
300 let config = get_remote_agent_config(None, None).await;
303 assert_eq!(
304 config.auth().ipc_cert_file_path().parent(),
305 default_auth_token_path.as_path().parent()
306 );
307 assert_eq!(
308 config.auth().ipc_cert_file_path().file_name().map(Path::new),
309 Some(PlatformSettings::get_ipc_cert_filename())
310 );
311 }
312
313 #[tokio::test]
314 async fn ipc_cert_file_path_defaults() {
315 let default_auth_token_path = PlatformSettings::get_auth_token_path();
316
317 let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
320 assert_eq!(
321 config.auth().ipc_cert_file_path().parent(),
322 default_auth_token_path.as_path().parent()
323 );
324 assert_eq!(
325 config.auth().ipc_cert_file_path().file_name().map(Path::new),
326 Some(PlatformSettings::get_ipc_cert_filename())
327 );
328 }
329
330 #[tokio::test]
331 async fn ipc_cert_file_path_explicitly_set() {
332 let default_auth_token_path = PlatformSettings::get_auth_token_path();
333 let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
334
335 let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
337 assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
338 }
339
340 #[tokio::test]
341 async fn ipc_cert_file_path_custom_auth_token_path() {
342 let custom_auth_token_path = PathBuf::from("/secret/auth_token");
343
344 let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
347 assert_eq!(
348 config.auth().ipc_cert_file_path().parent(),
349 custom_auth_token_path.as_path().parent()
350 );
351 assert_eq!(
352 config.auth().ipc_cert_file_path().file_name().map(Path::new),
353 Some(PlatformSettings::get_ipc_cert_filename())
354 );
355 }
356
357 #[tokio::test]
358 async fn ipc_cert_file_path_invalid_auth_token_path() {
359 let invalid_auth_token_path = PathBuf::from("/");
360
361 let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
364 assert_eq!(
365 config.auth().ipc_cert_file_path().parent(),
366 Some(PlatformSettings::get_config_dir_path())
367 );
368 assert_eq!(
369 config.auth().ipc_cert_file_path().file_name().map(Path::new),
370 Some(PlatformSettings::get_ipc_cert_filename())
371 );
372 }
373
374 async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
375 let (base_config, _) =
376 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
377 RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
378 }
379
380 #[tokio::test]
381 async fn endpoint_defaults_to_port_5001() {
382 let config = config_from_values(serde_json::Map::new()).await;
383 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
384 }
385
386 #[tokio::test]
387 async fn endpoint_uses_cmd_port() {
388 let mut values = serde_json::Map::new();
389 values.insert("cmd_port".to_string(), 7777.into());
390 let config = config_from_values(values).await;
391 assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
392 }
393
394 #[cfg(target_os = "linux")]
395 #[tokio::test]
396 async fn vsock_addr_valid_values() {
397 let cases: &[(&str, Option<u32>)] = &[
399 ("", None),
400 ("host", Some(2)),
401 ("hypervisor", Some(0)),
402 ("local", Some(3)),
403 ];
404
405 for (input, expected_cid) in cases {
406 let mut values = serde_json::Map::new();
407 values.insert("vsock_addr".to_string(), (*input).into());
408 values.insert("cmd_port".to_string(), 5001u16.into());
409 let config = config_from_values(values).await;
410 let result = config
411 .vsock_addr()
412 .expect("vsock_addr() should not error with cmd_port set");
413 assert_eq!(result.map(|a| a.cid()), *expected_cid, "input: {input:?}");
414 }
415 }
416
417 #[cfg(target_os = "linux")]
418 #[tokio::test]
419 async fn vsock_addr_invalid_values() {
420 let cases = &["invalid", "2", "HOST", "host ", "vm0"];
421
422 for input in cases {
423 let mut values = serde_json::Map::new();
424 values.insert("vsock_addr".to_string(), (*input).into());
425 let (base_config, _) =
426 ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
427 assert!(
428 RemoteAgentClientConfiguration::from_configuration(&base_config).is_err(),
429 "expected error for input: {input:?}",
430 );
431 }
432 }
433}