Skip to content

Commit 9bd0c20

Browse files
committed
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 abc4225 commit 9bd0c20

1 file changed

Lines changed: 52 additions & 0 deletions

File tree

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

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,28 @@ fn normalize_datadog_input_forms(merged: &mut serde_json::Value) {
270270
*value = parsed;
271271
}
272272
}
273+
274+
// `additional_endpoints` is a `HashMap<String, Vec<String>>` in the schema, but the deleted
275+
// component serde accepted two wider shapes the generated deserializer rejects: the whole value
276+
// as a JSON string (the form an env var produces, for example
277+
// `DD_ADDITIONAL_ENDPOINTS='{"https://app.datadoghq.com":["key"]}'`), and a bare string in place
278+
// of a one-element key list for a host. Restore both so dual-shipping config keeps deserializing.
279+
// First parse the JSON-string form into the object the deserializer expects; if it is not valid
280+
// JSON, leave the string in place so the downstream deserializer surfaces the error.
281+
if let Some(value @ serde_json::Value::String(_)) = object.get_mut("additional_endpoints") {
282+
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(value.as_str().expect("value matched String")) {
283+
*value = parsed;
284+
}
285+
}
286+
// Then wrap any bare-string host value into a one-element array (matching the old `OneOrMany`).
287+
if let Some(serde_json::Value::Object(map)) = object.get_mut("additional_endpoints") {
288+
for keys in map.values_mut() {
289+
if matches!(keys, serde_json::Value::String(_)) {
290+
let single = std::mem::take(keys);
291+
*keys = serde_json::Value::Array(vec![single]);
292+
}
293+
}
294+
}
273295
}
274296

275297
/// Translates the Datadog and Saluki-only sources into one [`SalukiConfiguration`], returning every
@@ -585,6 +607,36 @@ mod tests {
585607
}
586608
}
587609

610+
/// `additional_endpoints` accepts a JSON string (the Agent's env-var form) and a bare string in
611+
/// place of a one-element key list, as well as the native map-of-lists, all landing on
612+
/// `shared.endpoints.additional_endpoints`.
613+
#[tokio::test]
614+
async fn additional_endpoints_accepts_json_string_scalar_or_map() {
615+
let expected: std::collections::HashMap<String, Vec<String>> =
616+
[("https://app.datadoghq.com".to_string(), vec!["key".to_string()])]
617+
.into_iter()
618+
.collect();
619+
620+
for value in [
621+
// Env-var form: the whole map arrives as a JSON string.
622+
json!("{\"https://app.datadoghq.com\":[\"key\"]}"),
623+
// Env-var form with a bare string value in place of a one-element key list.
624+
json!("{\"https://app.datadoghq.com\":\"key\"}"),
625+
// Native map with a bare string in place of a one-element key list.
626+
json!({ "https://app.datadoghq.com": "key" }),
627+
// Native map of key lists.
628+
json!({ "https://app.datadoghq.com": ["key"] }),
629+
] {
630+
let (raw_map, _) =
631+
ConfigurationLoader::for_tests(Some(json!({ "additional_endpoints": value })), None, false).await;
632+
633+
let system = ConfigurationSystem::load(raw_map, EnvOverlayMode::Fallback)
634+
.await
635+
.expect("additional_endpoints translates");
636+
assert_eq!(system.config().shared.endpoints.additional_endpoints, expected);
637+
}
638+
}
639+
588640
#[tokio::test]
589641
async fn mapper_profile_other_required_fields_remain_required() {
590642
let cases = [

0 commit comments

Comments
 (0)