Skip to content

Commit 8047571

Browse files
authored
fix(config): accept the wider additional_endpoints input forms (#1994)
## Human Summary Caught by AI when reviewing #1993, it looks like this was a regression in the environment variable path only, which *should not* have affected customers anyway, but it looks like now we handle JSON in environment variable correctly if we happen to ever read that. ## AI Summary Restores the wider `additional_endpoints` input shapes that were lost when the metrics encoder and forwarder moved off the component serde onto the typed model. The old `AdditionalEndpoints` serde used `PickFirst<(DisplayFromStr, _)>` plus `OneOrMany`, so it accepted: - the whole map as a JSON string — the form an environment variable produces, e.g. `DD_ADDITIONAL_ENDPOINTS='{"https://app.datadoghq.com":["key"]}'`, and - a bare string in place of a one-element key list for a host. The generated `DatadogConfiguration` deserializes `additional_endpoints` as a plain `HashMap<String, Vec<String>>`, and the env overlay only materializes scalar/space-separated-list env values, so neither shape survives. Both now fail deserialization outright — a dual-shipping deployment configured through the environment fails to start. This adds an `additional_endpoints` case to `normalize_datadog_input_forms` (the same pass that already restores the `dogstatsd_eol_required` and `dogstatsd_mapper_profiles` env-var forms): parse the JSON-string form into an object, then wrap any bare-string host value into a one-element array. Invalid JSON is left in place so the downstream deserializer still surfaces the error. ## Change Type - [x] Bug fix ## How did you test this PR? - `cargo nextest run -p agent-data-plane-config-system` (added `additional_endpoints_accepts_json_string_scalar_or_map`, covering the JSON-string, JSON-string-with-scalar, native-scalar, and native-list shapes; confirmed it fails without the fix with `invalid type: string ..., expected a map`). - `make fmt` ## References - Merges into `m/pr5-cutover`
1 parent dbdba58 commit 8047571

1 file changed

Lines changed: 51 additions & 0 deletions

File tree

  • lib/agent-data-plane-config-system/src

lib/agent-data-plane-config-system/src/system.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,28 @@ fn normalize_datadog_input_forms(merged: &mut serde_json::Value) {
256256
*value = parsed;
257257
}
258258
}
259+
260+
// `additional_endpoints` is a `HashMap<String, Vec<String>>` in the schema, but the deleted
261+
// component serde accepted two wider shapes the generated deserializer rejects: the whole value
262+
// as a JSON string (the form an env var produces, for example
263+
// `DD_ADDITIONAL_ENDPOINTS='{"https://app.datadoghq.com":["key"]}'`), and a bare string in place
264+
// of a one-element key list for a host. Restore both so dual-shipping config keeps deserializing.
265+
// First parse the JSON-string form into the object the deserializer expects; if it is not valid
266+
// JSON, leave the string in place so the downstream deserializer surfaces the error.
267+
if let Some(value @ serde_json::Value::String(_)) = object.get_mut("additional_endpoints") {
268+
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(value.as_str().expect("value matched String")) {
269+
*value = parsed;
270+
}
271+
}
272+
// Then wrap any bare-string host value into a one-element array (matching the old `OneOrMany`).
273+
if let Some(serde_json::Value::Object(map)) = object.get_mut("additional_endpoints") {
274+
for keys in map.values_mut() {
275+
if matches!(keys, serde_json::Value::String(_)) {
276+
let single = std::mem::take(keys);
277+
*keys = serde_json::Value::Array(vec![single]);
278+
}
279+
}
280+
}
259281
}
260282

261283
/// Translates the Datadog and Saluki-only sources into one [`SalukiConfiguration`], returning every
@@ -558,6 +580,35 @@ mod tests {
558580
}
559581
}
560582

583+
/// `additional_endpoints` accepts a JSON string (the Agent's env-var form) and a bare string in
584+
/// place of a one-element key list, as well as the native map-of-lists, all landing on
585+
/// `shared.endpoints.additional_endpoints`.
586+
#[tokio::test]
587+
async fn additional_endpoints_accepts_json_string_scalar_or_map() {
588+
let expected: std::collections::HashMap<String, Vec<String>> =
589+
[("https://app.datadoghq.com".to_string(), vec!["key".to_string()])]
590+
.into_iter()
591+
.collect();
592+
593+
for value in [
594+
// Env-var form: the whole map arrives as a JSON string.
595+
json!("{\"https://app.datadoghq.com\":[\"key\"]}"),
596+
// Env-var form with a bare string value in place of a one-element key list.
597+
json!("{\"https://app.datadoghq.com\":\"key\"}"),
598+
// Native map with a bare string in place of a one-element key list.
599+
json!({ "https://app.datadoghq.com": "key" }),
600+
// Native map of key lists.
601+
json!({ "https://app.datadoghq.com": ["key"] }),
602+
] {
603+
let (raw_map, _) =
604+
ConfigurationLoader::for_tests(Some(json!({ "additional_endpoints": value })), None, false).await;
605+
606+
let system =
607+
ConfigurationSystem::load(raw_map, EnvOverlayMode::Fallback).expect("additional_endpoints translates");
608+
assert_eq!(system.config().shared.endpoints.additional_endpoints, expected);
609+
}
610+
}
611+
561612
#[tokio::test]
562613
async fn mapper_profile_other_required_fields_remain_required() {
563614
let cases = [

0 commit comments

Comments
 (0)