Skip to main content

datadog_agent_commons/ipc/
config.rs

1//! IPC configuration.
2
3use 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
11use crate::platform::PlatformSettings;
12
13fn default_agent_ipc_endpoint() -> Uri {
14    Uri::from_static("https://127.0.0.1:5001")
15}
16
17const fn default_connect_retry_attempts() -> usize {
18    10
19}
20
21const fn default_grpc_max_message_size() -> usize {
22    128 * 1024 * 1024
23}
24
25const fn default_connect_retry_backoff() -> Duration {
26    Duration::from_secs(2)
27}
28
29/// Datadog Agent IPC authentication configuration.
30#[derive(Deserialize)]
31#[serde(default)]
32pub struct IpcAuthConfiguration {
33    /// Path to the Agent authentication token file.
34    ///
35    /// The contents of the file are passed as a bearer token in RPC requests to the IPC endpoint.
36    ///
37    /// Defaults to `<conf dir>/auth_token`, where `<conf dir>` is the platform-specific directory containing the Agent
38    /// configuration.
39    auth_token_file_path: PathBuf,
40
41    /// Path to the Agent IPC TLS certificate file.
42    ///
43    /// The file is expected to be PEM-encoded, containing both a certificate and private key. The certificate will be
44    /// used to verify the TLS server certificate presented by the Agent, and the certificate and private key will be
45    /// used together to provide client authentication _to_ the Agent.
46    ///
47    /// Defaults to `ipc_cert.pem` in the same directory as the Agent authentication token file. (for example, if
48    /// `auth_token_file_path` is `/etc/datadog-agent/auth_token`, this will be `/etc/datadog-agent/ipc_cert.pem`.)
49    ipc_cert_file_path: Option<PathBuf>,
50}
51
52impl IpcAuthConfiguration {
53    // Creates a new `IpcAuthConfiguration` from the given configuration.
54    ///
55    /// ## Errors
56    ///
57    /// If the configuration is invalid, an error is returned.
58    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
59        config
60            .as_typed::<Self>()
61            .error_context("Failed to parse Datadog Agent IPC authentication configuration.")
62    }
63
64    /// Gets the path to the Agent authentication token file from the configuration.
65    pub fn auth_token_file_path(&self) -> PathBuf {
66        if self.auth_token_file_path.as_os_str().is_empty() {
67            return PlatformSettings::get_auth_token_path();
68        }
69
70        self.auth_token_file_path.clone()
71    }
72
73    /// Gets the IPC certificate file path from the configuration.
74    pub fn ipc_cert_file_path(&self) -> PathBuf {
75        // If the IPC cert file path is set explicitly, we always prefer that.
76        if let Some(path) = self.ipc_cert_file_path.as_ref() {
77            if !path.as_os_str().is_empty() {
78                return path.clone();
79            }
80        }
81
82        // Otherwise, we default to the same directory as the auth token file with the default certificate file name.
83        let auth_token_dir = if self.auth_token_file_path.as_os_str().is_empty() {
84            PlatformSettings::get_config_dir_path()
85        } else {
86            self.auth_token_file_path
87                .parent()
88                .unwrap_or(PlatformSettings::get_config_dir_path())
89        };
90
91        auth_token_dir.join(PlatformSettings::get_ipc_cert_filename())
92    }
93}
94
95impl Default for IpcAuthConfiguration {
96    fn default() -> Self {
97        Self {
98            auth_token_file_path: PlatformSettings::get_auth_token_path(),
99            ipc_cert_file_path: None,
100        }
101    }
102}
103
104/// Datadog Agent IPC client configuration.
105#[derive(Deserialize)]
106pub struct RemoteAgentClientConfiguration {
107    /// Datadog Agent IPC endpoint to connect to.
108    ///
109    /// Caution/weird: This is configuration is only available on agent-data-plane, and would allow
110    /// one to connect to an Agent at a URI other than localhost/127.0.0.1. However, the Datadog
111    /// configuration schema doesn't account for this and instead provides `cmd_port`. Therefore,
112    ///
113    /// **CAUTION**: if `cmd_port` is set, then `ipc_endpoint` is ignored.
114    ///
115    /// Defaults to `https://127.0.0.1:5001`.
116    #[serde(
117        rename = "agent_ipc_endpoint",
118        with = "http_serde_ext::uri",
119        default = "default_agent_ipc_endpoint"
120    )]
121    ipc_endpoint: Uri,
122
123    /// The port that will be used to connect to the Datadog Agent IPC on the local host.
124    ///
125    /// Takes precedence over `ipc_endpoint` if set.
126    cmd_port: Option<u16>,
127
128    /// Authentication configuration for the IPC endpoint.
129    #[serde(flatten, default)]
130    auth: IpcAuthConfiguration,
131
132    /// Number of allowed retry attempts when initially connecting.
133    ///
134    /// Defaults to `10`.
135    #[serde(default = "default_connect_retry_attempts")]
136    connect_retry_attempts: usize,
137
138    /// Amount of time to wait between connection attempts when initially connecting.
139    ///
140    /// Defaults to 2 seconds.
141    #[serde(default = "default_connect_retry_backoff")]
142    connect_retry_backoff: Duration,
143
144    /// Maximum message size for gRPC messages.
145    ///
146    /// Defaults to `128 * 1024 * 1024` (128 MB).
147    #[serde(
148        rename = "agent_ipc_grpc_max_message_size",
149        default = "default_grpc_max_message_size"
150    )]
151    grpc_max_message_size: usize,
152}
153
154impl RemoteAgentClientConfiguration {
155    /// Creates a new `RemoteAgentClientConfiguration` from the given configuration.
156    ///
157    /// ## Errors
158    ///
159    /// If the configuration is invalid, an error is returned.
160    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
161        config
162            .as_typed::<Self>()
163            .error_context("Failed to parse Datadog Agent IPC client configuration.")
164    }
165
166    /// Returns a reference to the authentication configuration for the Remote Agent client.
167    pub fn auth(&self) -> &IpcAuthConfiguration {
168        &self.auth
169    }
170
171    /// Returns the IPC endpoint URI.
172    ///
173    /// If `cmd_port` is set, the endpoint is built from it, ignoring `ipc_endpoint`.
174    pub fn endpoint(&self) -> Result<Uri, GenericError> {
175        if let Some(cmd_port) = self.cmd_port {
176            format!("https://127.0.0.1:{}", cmd_port)
177                .parse::<Uri>()
178                .with_error_context(|| format!("failed to build URI from cmd_port {cmd_port}"))
179        } else {
180            Ok(self.ipc_endpoint.clone())
181        }
182    }
183
184    /// Returns the maximum message size for gRPC.
185    pub fn grpc_max_message_size(&self) -> usize {
186        self.grpc_max_message_size
187    }
188}
189
190impl BackoffBuilder for &RemoteAgentClientConfiguration {
191    type Backoff = <ConstantBuilder as BackoffBuilder>::Backoff;
192
193    fn build(self) -> Self::Backoff {
194        ConstantBuilder::default()
195            .with_delay(self.connect_retry_backoff)
196            .with_max_times(self.connect_retry_attempts)
197            .build()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use std::path::{Path, PathBuf};
204
205    use saluki_config::ConfigurationLoader;
206
207    use super::RemoteAgentClientConfiguration;
208    use crate::platform::PlatformSettings;
209
210    async fn get_remote_agent_config(
211        ipc_cert_file_path: Option<&Path>, auth_token_file_path: Option<&Path>,
212    ) -> RemoteAgentClientConfiguration {
213        // Set the values in the config map if provided.
214        let mut values = serde_json::Map::new();
215        if let Some(path) = ipc_cert_file_path {
216            values.insert(
217                "ipc_cert_file_path".to_string(),
218                path.to_string_lossy().into_owned().into(),
219            );
220        }
221        if let Some(path) = auth_token_file_path {
222            values.insert(
223                "auth_token_file_path".to_string(),
224                path.to_string_lossy().into_owned().into(),
225            );
226        }
227
228        let (base_config, _) =
229            ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
230        RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
231    }
232
233    #[tokio::test]
234    async fn ipc_cert_file_path_empty_config() {
235        let default_auth_token_path = PlatformSettings::get_auth_token_path();
236
237        // When the auth token file path _and_ IPC cert file path are both unset, we should default to looking for the
238        // IPC cert in the same directory as the auth token.
239        let config = get_remote_agent_config(None, None).await;
240        assert_eq!(
241            config.auth().ipc_cert_file_path().parent(),
242            default_auth_token_path.as_path().parent()
243        );
244        assert_eq!(
245            config.auth().ipc_cert_file_path().file_name().map(Path::new),
246            Some(PlatformSettings::get_ipc_cert_filename())
247        );
248    }
249
250    #[tokio::test]
251    async fn ipc_cert_file_path_defaults() {
252        let default_auth_token_path = PlatformSettings::get_auth_token_path();
253
254        // When the IPC cert file path is not set, it should default to the same directory as the auth token file using
255        // the default certificate file name.
256        let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
257        assert_eq!(
258            config.auth().ipc_cert_file_path().parent(),
259            default_auth_token_path.as_path().parent()
260        );
261        assert_eq!(
262            config.auth().ipc_cert_file_path().file_name().map(Path::new),
263            Some(PlatformSettings::get_ipc_cert_filename())
264        );
265    }
266
267    #[tokio::test]
268    async fn ipc_cert_file_path_explicitly_set() {
269        let default_auth_token_path = PlatformSettings::get_auth_token_path();
270        let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
271
272        // When the IPC cert file path is explicitly set, it should be used.
273        let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
274        assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
275    }
276
277    #[tokio::test]
278    async fn ipc_cert_file_path_custom_auth_token_path() {
279        let custom_auth_token_path = PathBuf::from("/secret/auth_token");
280
281        // When the IPC cert file path is not set, but there's a custom auth token path (explicitly set, different from the default),
282        // we should still look in the same directory as the auth token file using the default certificate file name.
283        let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
284        assert_eq!(
285            config.auth().ipc_cert_file_path().parent(),
286            custom_auth_token_path.as_path().parent()
287        );
288        assert_eq!(
289            config.auth().ipc_cert_file_path().file_name().map(Path::new),
290            Some(PlatformSettings::get_ipc_cert_filename())
291        );
292    }
293
294    #[tokio::test]
295    async fn ipc_cert_file_path_invalid_auth_token_path() {
296        let invalid_auth_token_path = PathBuf::from("/");
297
298        // If the auth token file path is somehow unset or invalid (for example, no parent directory), we should use the same
299        // logic but with the default Datadog Agent configuration directory.
300        let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
301        assert_eq!(
302            config.auth().ipc_cert_file_path().parent(),
303            Some(PlatformSettings::get_config_dir_path())
304        );
305        assert_eq!(
306            config.auth().ipc_cert_file_path().file_name().map(Path::new),
307            Some(PlatformSettings::get_ipc_cert_filename())
308        );
309    }
310
311    async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
312        let (base_config, _) =
313            ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
314        RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
315    }
316
317    #[tokio::test]
318    async fn endpoint_defaults_to_port_5001() {
319        let config = config_from_values(serde_json::Map::new()).await;
320        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
321    }
322
323    #[tokio::test]
324    async fn endpoint_uses_cmd_port() {
325        let mut values = serde_json::Map::new();
326        values.insert("cmd_port".to_string(), 7777.into());
327        let config = config_from_values(values).await;
328        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
329    }
330
331    #[tokio::test]
332    async fn cmd_port_takes_precedence_over_ipc_endpoint() {
333        let mut values = serde_json::Map::new();
334        values.insert("cmd_port".to_string(), 8888.into());
335        values.insert("agent_ipc_endpoint".to_string(), "https://10.0.0.1:3333".into());
336        let config = config_from_values(values).await;
337        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:8888/");
338    }
339
340    #[tokio::test]
341    async fn ipc_endpoint_used_when_no_cmd_port() {
342        let mut values = serde_json::Map::new();
343        values.insert("agent_ipc_endpoint".to_string(), "https://10.0.0.1:3333".into());
344        let config = config_from_values(values).await;
345        assert_eq!(config.endpoint().unwrap().to_string(), "https://10.0.0.1:3333/");
346    }
347}