agent_data_plane_config_system/
loaded.rs1use std::path::Path;
10
11use agent_data_plane_config::SalukiConfiguration;
12use datadog_agent_config::apply_datadog_env;
13use datadog_agent_config::{DatadogRemapper, EnvOverlayMode, KEY_ALIASES};
15use saluki_config::dynamic::ConfigUpdate;
16use saluki_config::{ConfigurationLoader, GenericConfiguration};
17use serde_json::Value;
18use tokio::sync::mpsc;
19
20use crate::saluki_env_overlay;
21use crate::system::{translate_strict, ConfigurationSystem, Error};
22
23const ENV_VAR_PREFIX: &str = "DD";
27
28const COMPAT_FORWARD_CHANNEL_SIZE: usize = 100;
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum EnvPrecedence {
38 BeforeFile,
42 AfterFile,
45 Disabled,
47}
48
49impl EnvPrecedence {
50 fn overlay_mode(self) -> EnvOverlayMode {
56 match self {
57 EnvPrecedence::Disabled => EnvOverlayMode::Disabled,
58 EnvPrecedence::AfterFile | EnvPrecedence::BeforeFile => EnvOverlayMode::Fallback,
59 }
60 }
61}
62
63pub struct LoadedConfiguration {
68 loader: ConfigurationLoader,
69 base: Value,
71 env: EnvPrecedence,
72 local: SalukiConfiguration,
75}
76
77impl LoadedConfiguration {
78 pub async fn load(path: impl AsRef<Path>, env: EnvPrecedence) -> Result<Self, Error> {
84 let loader = build_loader(path.as_ref(), env)?;
85 let base = build_base(path.as_ref(), env)?;
86 let local = translate_strict(&base, env.overlay_mode())?;
87 Ok(Self {
88 loader,
89 base,
90 env,
91 local,
92 })
93 }
94
95 pub fn local(&self) -> &SalukiConfiguration {
99 &self.local
100 }
101
102 pub fn raw_config(&self) -> GenericConfiguration {
105 self.loader.bootstrap_generic()
106 }
107
108 pub async fn run(self, config_stream: mpsc::Receiver<ConfigUpdate>) -> Result<ConfigurationSystem, Error> {
118 let (compat_tx, compat_rx) = mpsc::channel(COMPAT_FORWARD_CHANNEL_SIZE);
121 let compat_map = self.loader.with_dynamic_configuration(compat_rx).into_generic().await?;
122
123 ConfigurationSystem::connected(config_stream, compat_tx, compat_map, self.base, self.env.overlay_mode()).await
124 }
125
126 pub async fn standalone(self) -> Result<ConfigurationSystem, Error> {
134 let compat_map = self.loader.into_generic().await?;
135 Ok(ConfigurationSystem::standalone(compat_map, self.local))
136 }
137}
138
139fn build_base(path: &Path, env: EnvPrecedence) -> Result<Value, Error> {
148 let text = std::fs::read_to_string(path).map_err(|e| Error::Base {
149 message: format!("read `{}`: {e}", path.display()),
150 })?;
151 let mut base: Value = serde_yaml::from_str(&text).map_err(|e| Error::Base {
152 message: format!("parse `{}`: {e}", path.display()),
153 })?;
154 drop_nulls(&mut base);
155 if base.is_null() {
156 base = Value::Object(serde_json::Map::new());
157 }
158
159 let overwrite = match env {
160 EnvPrecedence::Disabled => return Ok(base),
161 EnvPrecedence::AfterFile => true,
162 EnvPrecedence::BeforeFile => false,
163 };
164 apply_datadog_env(&mut base, overwrite).map_err(|message| Error::Base { message })?;
165 saluki_env_overlay::apply_env(&mut base, overwrite).map_err(|message| Error::Base { message })?;
166 Ok(base)
167}
168
169fn drop_nulls(value: &mut Value) {
172 if let Value::Object(map) = value {
173 map.retain(|_, v| !v.is_null());
174 for v in map.values_mut() {
175 drop_nulls(v);
176 }
177 }
178}
179
180fn build_loader(path: &Path, env: EnvPrecedence) -> Result<ConfigurationLoader, Error> {
183 let loader = ConfigurationLoader::default().with_key_aliases(KEY_ALIASES);
184 let loader = match env {
185 EnvPrecedence::AfterFile => loader
186 .from_yaml(path)?
187 .add_providers([DatadogRemapper::new()])
188 .from_environment(ENV_VAR_PREFIX)?,
189 EnvPrecedence::BeforeFile => loader
190 .add_providers([DatadogRemapper::new()])
191 .from_environment(ENV_VAR_PREFIX)?
192 .from_yaml(path)?,
193 EnvPrecedence::Disabled => loader.from_yaml(path)?,
194 };
195 Ok(loader)
196}
197
198#[cfg(test)]
199mod tests {
200 use bytesize::ByteSize;
201 use serde_json::json;
202
203 use super::*;
204
205 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
207
208 #[test]
209 fn build_base_composes_file_and_environment_by_precedence() {
210 let _guard = ENV_MUTEX.lock().unwrap();
211 let path = std::env::temp_dir().join(format!("adp_build_base_{}.yaml", std::process::id()));
212 std::fs::write(
213 &path,
214 "dogstatsd_port: 8125\ndogstatsd_non_local_traffic: false\nempty_key:\n",
215 )
216 .unwrap();
217 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
218
219 let base = build_base(&path, EnvPrecedence::AfterFile).expect("base builds");
222 assert_eq!(base.get("dogstatsd_port"), Some(&json!(9125)));
223 assert_eq!(base.get("dogstatsd_non_local_traffic"), Some(&json!(false)));
224 assert!(base.get("empty_key").is_none());
225
226 let base = build_base(&path, EnvPrecedence::BeforeFile).expect("base builds");
228 assert_eq!(base.get("dogstatsd_port"), Some(&json!(8125)));
229
230 let base = build_base(&path, EnvPrecedence::Disabled).expect("base builds");
232 assert_eq!(base.get("dogstatsd_port"), Some(&json!(8125)));
233
234 std::env::remove_var("DD_DOGSTATSD_PORT");
235 std::fs::remove_file(&path).ok();
236 }
237
238 #[tokio::test]
239 async fn local_exposes_translated_configuration() {
240 let path = std::env::temp_dir().join(format!("adp_local_{}.yaml", std::process::id()));
242 std::fs::write(&path, "log_level: warn\ndogstatsd_port: 9125\n").unwrap();
243
244 let loaded = LoadedConfiguration::load(&path, EnvPrecedence::Disabled)
245 .await
246 .expect("local sources load");
247 let config = loaded.local();
248
249 assert_eq!(config.control.logging.level, "warn");
250 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
251
252 std::fs::remove_file(&path).ok();
253 }
254
255 #[tokio::test]
256 async fn load_rejects_translation_invalid_local_sources() {
257 let path = std::env::temp_dir().join(format!("adp_local_bad_{}.yaml", std::process::id()));
258 std::fs::write(&path, "dogstatsd_tag_cardinality: bogus\n").unwrap();
260
261 let result = LoadedConfiguration::load(&path, EnvPrecedence::Disabled).await;
262
263 std::fs::remove_file(&path).ok();
264 assert!(matches!(result, Err(Error::Translate { .. })));
265 }
266
267 #[test]
268 fn build_base_rejects_a_malformed_environment_value() {
269 let _guard = ENV_MUTEX.lock().unwrap();
270 let path = std::env::temp_dir().join(format!("adp_build_base_bad_{}.yaml", std::process::id()));
271 std::fs::write(&path, "dogstatsd_port: 8125\n").unwrap();
272 std::env::set_var("DD_DOGSTATSD_PORT", "not-a-number");
273
274 let result = build_base(&path, EnvPrecedence::AfterFile);
275
276 std::env::remove_var("DD_DOGSTATSD_PORT");
277 std::fs::remove_file(&path).ok();
278 assert!(matches!(result, Err(Error::Base { .. })));
279 }
280
281 #[test]
282 fn build_base_accepts_a_human_readable_dogstatsd_interner_size() {
283 let _guard = ENV_MUTEX.lock().unwrap();
284 let path = std::env::temp_dir().join(format!("adp_build_base_interner_{}.yaml", std::process::id()));
285 std::fs::write(&path, "{}\n").unwrap();
286 std::env::set_var("DD_DOGSTATSD_STRING_INTERNER_SIZE_BYTES", "12MiB");
287
288 let base = build_base(&path, EnvPrecedence::AfterFile).expect("human-readable byte size builds");
289 let config = translate_strict(&base, EnvOverlayMode::Fallback).expect("human-readable byte size translates");
290
291 std::env::remove_var("DD_DOGSTATSD_STRING_INTERNER_SIZE_BYTES");
292 std::fs::remove_file(&path).ok();
293 assert_eq!(
294 config.domains.dogstatsd.contexts.string_interner_size_bytes,
295 Some(ByteSize::mib(12).as_u64())
296 );
297 }
298}