agent_data_plane_config_system/
env_provider.rs

1//! A Figment provider that reads environment variables into their canonical configuration shape.
2//!
3//! The by-key configuration view is backed by Figment, whose environment provider can only split a
4//! variable name on a fixed separator. The Datadog Agent does not name its variables that way: it
5//! iterates a table of known keys and looks up each key's declared variable names, so
6//! `DD_PROXY_HTTP` reaches `proxy.http` while `DD_DOGSTATSD_PORT` reaches the flat `dogstatsd_port`.
7//! Nothing about the two names says where a nesting boundary falls.
8//!
9//! [`EnvironmentProvider`] resolves that by reusing the same schema-driven readers the typed
10//! configuration path uses: [`apply_datadog_env`] for keys the vendored Datadog schema declares, and
11//! the Saluki-only reader for keys it does not. Both write each value at its canonical nested path,
12//! so a consumer deserializing the Agent's real configuration shape sees environment values at the
13//! same place it sees file and Agent-stream values.
14//!
15//! Add this provider alongside the prefix-scanning environment provider rather than in place of it.
16//! The scanning provider still supplies flat keys that no model declares, which by-key consumers
17//! read directly.
18
19use datadog_agent_config::apply_datadog_env;
20use figment::providers::Serialized;
21use figment::value::{Dict, Map};
22use figment::{Error, Metadata, Profile, Provider};
23use serde_json::Value;
24
25use crate::saluki_env_overlay;
26
27/// A Figment provider carrying every modeled configuration key set in the environment, at its
28/// canonical path.
29///
30/// Values are snapshotted at construction time.
31pub struct EnvironmentProvider {
32    values: Value,
33}
34
35impl EnvironmentProvider {
36    /// Reads the process environment for every modeled Datadog and Saluki-only key.
37    ///
38    /// Only keys whose environment variables are set to a non-empty value appear in the provider; it
39    /// contributes no defaults, so it never masks a lower-precedence source.
40    ///
41    /// # Errors
42    ///
43    /// Returns a message naming the environment variable when its value is malformed for the shape
44    /// its key declares. This is the same input the typed configuration path rejects at startup, so
45    /// failing here keeps the two views from disagreeing about what loaded successfully.
46    pub fn new() -> Result<Self, String> {
47        let mut values = Value::Object(serde_json::Map::new());
48
49        // The base starts empty, so nothing can be overwritten; `true` avoids a redundant
50        // path-presence check per key.
51        apply_datadog_env(&mut values, true)?;
52        saluki_env_overlay::apply_env(&mut values, true)?;
53
54        Ok(Self { values })
55    }
56}
57
58impl Provider for EnvironmentProvider {
59    fn metadata(&self) -> Metadata {
60        // Names both key classes, because this provider carries both. `DatadogEnvProvider` reads
61        // only the schema-declared keys and names itself accordingly.
62        Metadata::named("Datadog and Saluki-only environment variables")
63    }
64
65    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
66        Serialized::defaults(&self.values).data()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use saluki_config::test_env_lock;
73
74    use super::*;
75
76    fn values_of(provider: &EnvironmentProvider) -> &Value {
77        &provider.values
78    }
79
80    #[test]
81    fn nested_datadog_key_lands_at_its_canonical_path() {
82        let _guard = test_env_lock();
83        std::env::set_var("DD_PROXY_HTTP", "http://proxy.example.com");
84
85        let provider = EnvironmentProvider::new().expect("environment reads");
86
87        std::env::remove_var("DD_PROXY_HTTP");
88        assert_eq!(
89            values_of(&provider).pointer("/proxy/http"),
90            Some(&Value::String("http://proxy.example.com".to_string()))
91        );
92    }
93
94    #[test]
95    fn canonical_proxy_variable_is_honored() {
96        // `HTTP_PROXY` carries no `DD_` prefix, so the prefix-scanning provider cannot see it at all.
97        let _guard = test_env_lock();
98        std::env::remove_var("DD_PROXY_HTTP");
99        std::env::set_var("HTTP_PROXY", "http://canonical.example.com");
100
101        let provider = EnvironmentProvider::new().expect("environment reads");
102
103        std::env::remove_var("HTTP_PROXY");
104        assert_eq!(
105            values_of(&provider).pointer("/proxy/http"),
106            Some(&Value::String("http://canonical.example.com".to_string()))
107        );
108    }
109
110    #[test]
111    fn saluki_only_key_lands_at_its_canonical_path() {
112        let _guard = test_env_lock();
113        std::env::set_var("DD_DATA_PLANE_STANDALONE_MODE", "true");
114
115        let provider = EnvironmentProvider::new().expect("environment reads");
116
117        std::env::remove_var("DD_DATA_PLANE_STANDALONE_MODE");
118        assert_eq!(
119            values_of(&provider).pointer("/data_plane/standalone_mode"),
120            Some(&Value::Bool(true))
121        );
122    }
123
124    /// The documented environment variable must appear at the key's canonical nested path in the
125    /// provider output.
126    #[test]
127    fn adp_zstd_override_reaches_its_nested_path() {
128        let _guard = test_env_lock();
129        std::env::set_var("DD_DATA_PLANE_SERIALIZER_ZSTD_COMPRESSOR_LEVEL", "7");
130
131        let provider = EnvironmentProvider::new().expect("environment reads");
132
133        std::env::remove_var("DD_DATA_PLANE_SERIALIZER_ZSTD_COMPRESSOR_LEVEL");
134        assert_eq!(
135            values_of(&provider).pointer("/data_plane/serializer_zstd_compressor_level"),
136            Some(&Value::Number(7.into()))
137        );
138    }
139
140    #[test]
141    fn unset_keys_contribute_nothing() {
142        let _guard = test_env_lock();
143        std::env::remove_var("DD_PROXY_HTTP");
144        std::env::remove_var("HTTP_PROXY");
145        std::env::remove_var("http_proxy");
146
147        let provider = EnvironmentProvider::new().expect("environment reads");
148
149        assert!(values_of(&provider).pointer("/proxy/http").is_none());
150    }
151
152    #[test]
153    fn a_malformed_value_is_rejected() {
154        let _guard = test_env_lock();
155        std::env::set_var("DD_DOGSTATSD_PORT", "not-a-number");
156
157        let result = EnvironmentProvider::new();
158
159        std::env::remove_var("DD_DOGSTATSD_PORT");
160        assert!(result.is_err());
161    }
162}