Skip to content

Commit 90acf2a

Browse files
fix(config): support colons in tag values (URLs, etc.) (#953)
https://datadoghq.atlassian.net/browse/SVLS-8095 ## Overview Tag parsing previously used split(':') which broke values containing colons like URLs (git.repository_url:https://...). Changed to usesplitn(2, ':') to split only on the first colon, preserving the rest as the value. Changes: - Add parse_key_value_tag() helper to centralize parsing logic - Refactor deserialize_key_value_pairs to use helper - Refactor deserialize_key_value_pair_array_to_hashmap to use helper - Add comprehensive test coverage for URL values and edge cases ## Testing unit test and expect e2e tests to pass Co-authored-by: tianning.li <tianning.li@datadoghq.com>
1 parent 13d5bb1 commit 90acf2a

1 file changed

Lines changed: 93 additions & 16 deletions

File tree

mod.rs

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,21 @@ where
535535
}
536536
}
537537

538+
/// Parse a single "key:value" string into a (key, value) tuple
539+
/// Returns None if the string is invalid (e.g., missing colon, empty key/value)
540+
fn parse_key_value_tag(tag: &str) -> Option<(String, String)> {
541+
let parts: Vec<&str> = tag.splitn(2, ':').collect();
542+
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
543+
Some((parts[0].to_string(), parts[1].to_string()))
544+
} else {
545+
error!(
546+
"Failed to parse tag '{}', expected format 'key:value', ignoring",
547+
tag
548+
);
549+
None
550+
}
551+
}
552+
538553
pub fn deserialize_key_value_pairs<'de, D>(
539554
deserializer: D,
540555
) -> Result<HashMap<String, String>, D::Error>
@@ -559,14 +574,8 @@ where
559574
if tag.is_empty() {
560575
continue;
561576
}
562-
let parts = tag.split(':').collect::<Vec<&str>>();
563-
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
564-
map.insert(parts[0].to_string(), parts[1].to_string());
565-
} else {
566-
error!(
567-
"Failed to parse tag '{}', expected format 'key:value', ignoring",
568-
tag
569-
);
577+
if let Some((key, val)) = parse_key_value_tag(tag) {
578+
map.insert(key, val);
570579
}
571580
}
572581

@@ -643,14 +652,8 @@ where
643652
let array: Vec<String> = Vec::deserialize(deserializer)?;
644653
let mut map = HashMap::new();
645654
for s in array {
646-
let parts = s.split(':').collect::<Vec<&str>>();
647-
if parts.len() == 2 {
648-
map.insert(parts[0].to_string(), parts[1].to_string());
649-
} else {
650-
error!(
651-
"Failed to parse tag '{}', expected format 'key:value', ignoring",
652-
s.trim()
653-
);
655+
if let Some((key, val)) = parse_key_value_tag(&s) {
656+
map.insert(key, val);
654657
}
655658
}
656659
Ok(map)
@@ -1480,4 +1483,78 @@ pub mod tests {
14801483
expected.insert("valid".to_string(), "tag".to_string());
14811484
assert_eq!(result.tags, expected);
14821485
}
1486+
1487+
#[test]
1488+
fn test_deserialize_key_value_pairs_with_url_values() {
1489+
#[derive(Deserialize, Debug, PartialEq)]
1490+
struct TestStruct {
1491+
#[serde(deserialize_with = "deserialize_key_value_pairs")]
1492+
tags: HashMap<String, String>,
1493+
}
1494+
1495+
let result = serde_json::from_str::<TestStruct>(
1496+
r#"{"tags": "git.repository_url:https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git,env:prod"}"#
1497+
)
1498+
.expect("failed to parse JSON");
1499+
let mut expected = HashMap::new();
1500+
expected.insert(
1501+
"git.repository_url".to_string(),
1502+
"https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git".to_string(),
1503+
);
1504+
expected.insert("env".to_string(), "prod".to_string());
1505+
assert_eq!(result.tags, expected);
1506+
}
1507+
1508+
#[test]
1509+
fn test_deserialize_key_value_pair_array_with_urls() {
1510+
#[derive(Deserialize, Debug, PartialEq)]
1511+
struct TestStruct {
1512+
#[serde(deserialize_with = "deserialize_key_value_pair_array_to_hashmap")]
1513+
tags: HashMap<String, String>,
1514+
}
1515+
1516+
let result = serde_json::from_str::<TestStruct>(
1517+
r#"{"tags": ["git.repository_url:https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git", "env:prod", "version:1.2.3"]}"#
1518+
)
1519+
.expect("failed to parse JSON");
1520+
let mut expected = HashMap::new();
1521+
expected.insert(
1522+
"git.repository_url".to_string(),
1523+
"https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git".to_string(),
1524+
);
1525+
expected.insert("env".to_string(), "prod".to_string());
1526+
expected.insert("version".to_string(), "1.2.3".to_string());
1527+
assert_eq!(result.tags, expected);
1528+
}
1529+
1530+
#[test]
1531+
fn test_deserialize_key_value_pair_array_ignores_invalid() {
1532+
#[derive(Deserialize, Debug, PartialEq)]
1533+
struct TestStruct {
1534+
#[serde(deserialize_with = "deserialize_key_value_pair_array_to_hashmap")]
1535+
tags: HashMap<String, String>,
1536+
}
1537+
1538+
let result = serde_json::from_str::<TestStruct>(
1539+
r#"{"tags": ["valid:tag", "invalid_no_colon", "another:good:value:with:colons"]}"#,
1540+
)
1541+
.expect("failed to parse JSON");
1542+
let mut expected = HashMap::new();
1543+
expected.insert("valid".to_string(), "tag".to_string());
1544+
expected.insert("another".to_string(), "good:value:with:colons".to_string());
1545+
assert_eq!(result.tags, expected);
1546+
}
1547+
1548+
#[test]
1549+
fn test_deserialize_key_value_pair_array_empty() {
1550+
#[derive(Deserialize, Debug, PartialEq)]
1551+
struct TestStruct {
1552+
#[serde(deserialize_with = "deserialize_key_value_pair_array_to_hashmap")]
1553+
tags: HashMap<String, String>,
1554+
}
1555+
1556+
let result =
1557+
serde_json::from_str::<TestStruct>(r#"{"tags": []}"#).expect("failed to parse JSON");
1558+
assert_eq!(result.tags, HashMap::new());
1559+
}
14831560
}

0 commit comments

Comments
 (0)