@@ -310,10 +310,21 @@ async fn run_config_stream_event_loop(
310310 Some ( config_event:: Event :: Snapshot ( snapshot) ) => {
311311 Some ( ConfigUpdate :: Snapshot ( snapshot_to_settings ( & snapshot) ) )
312312 }
313- Some ( config_event:: Event :: Update ( update) ) => update
314- . setting
315- . as_ref ( )
316- . map ( |setting| ConfigUpdate :: Partial ( setting_to_config_setting ( setting) ) ) ,
313+ Some ( config_event:: Event :: Update ( update) ) => {
314+ update. setting . as_ref ( ) . and_then ( |setting| {
315+ match setting_to_config_setting ( setting) {
316+ Some ( converted) => Some ( ConfigUpdate :: Partial ( converted) ) ,
317+ None => {
318+ // Do not inject JSON null: typed fields interpret it as an invalid value.
319+ debug ! (
320+ key = %setting. key,
321+ "Ignoring a configuration update for a declared key with no value."
322+ ) ;
323+ None
324+ }
325+ }
326+ } )
327+ }
317328 None => {
318329 error ! ( "Received a configuration update event with no data." ) ;
319330 None
@@ -338,33 +349,35 @@ async fn run_config_stream_event_loop(
338349 }
339350}
340351
341- /// Agent source names for values the Agent supplied itself rather than an operator: `default` is a
342- /// schema default value, and `schema` is a declared key that has no value at all.
343- const AGENT_UNSET_SOURCES : [ & str ; 2 ] = [ "default" , "schema" ] ;
352+ // Source for a value supplied by the Agent's schema default.
353+ const AGENT_DEFAULT_SOURCE : & str = "default" ;
344354
345- /// Converts a setting from the Agent's RPC wire protocol to our `ConfigSetting` type.
346- ///
347- ///
348- /// An unrecognized `source` is treated as `Provenance::Explicit` because it is safer.
349- /// `Provenance::Default` values can be overwritten downstream so we want to be sure when labeling a
350- /// value as such.
351- fn setting_to_config_setting ( setting : & AgentConfigSetting ) -> ConfigSetting {
352- let provenance = if AGENT_UNSET_SOURCES . contains ( & setting. source . as_str ( ) ) {
355+ // Source for a schema-declared key with no configured value.
356+ const AGENT_DECLARED_ONLY_SOURCE : & str = "schema" ;
357+
358+ // Omit settings without values; JSON null is not a missing-value sentinel for typed fields.
359+ fn setting_to_config_setting ( setting : & AgentConfigSetting ) -> Option < ConfigSetting > {
360+ if setting. source == AGENT_DECLARED_ONLY_SOURCE {
361+ return None ;
362+ }
363+
364+ let value = proto_value_to_serde_value ( & setting. value ) ;
365+ if value. is_null ( ) {
366+ return None ;
367+ }
368+
369+ let provenance = if setting. source == AGENT_DEFAULT_SOURCE {
353370 Provenance :: Default
354371 } else {
355372 Provenance :: Explicit
356373 } ;
357374
358- ConfigSetting :: new (
359- setting. key . clone ( ) ,
360- proto_value_to_serde_value ( & setting. value ) ,
361- provenance,
362- )
375+ Some ( ConfigSetting :: new ( setting. key . clone ( ) , value, provenance) )
363376}
364377
365- /// Converts a `ConfigSnapshot` into the settings it carries .
378+ // A snapshot must not pass valueless settings to the typed configuration layer .
366379fn snapshot_to_settings ( snapshot : & ConfigSnapshot ) -> Vec < ConfigSetting > {
367- snapshot. settings . iter ( ) . map ( setting_to_config_setting) . collect ( )
380+ snapshot. settings . iter ( ) . filter_map ( setting_to_config_setting) . collect ( )
368381}
369382
370383/// Recursively converts a `google::protobuf::Value` into a `serde_json::Value`.
@@ -908,32 +921,77 @@ mod tests {
908921 }
909922
910923 #[ test]
911- fn agent_supplied_sources_are_marked_as_defaults ( ) {
912- for source in AGENT_UNSET_SOURCES {
913- let setting = setting_to_config_setting ( & agent_setting ( source, "dd_url" , "https://app.datadoghq.com" ) ) ;
924+ fn an_agent_default_is_marked_as_a_default ( ) {
925+ let setting = setting_to_config_setting ( & agent_setting (
926+ AGENT_DEFAULT_SOURCE ,
927+ "dd_url" ,
928+ "https://app.datadoghq.com" ,
929+ ) )
930+ . expect ( "a defaulted setting has a value" ) ;
931+
932+ assert_eq ! ( setting. key, "dd_url" ) ;
933+ assert_eq ! ( setting. value, Value :: from( "https://app.datadoghq.com" ) ) ;
934+ assert_eq ! ( setting. provenance, Provenance :: Default ) ;
935+ }
914936
915- assert_eq ! ( setting. key, "dd_url" ) ;
916- assert_eq ! ( setting. value, Value :: from( "https://app.datadoghq.com" ) ) ;
917- assert_eq ! (
918- setting. provenance,
919- Provenance :: Default ,
920- "source {source} should be a default"
937+ #[ test]
938+ fn a_declared_key_with_no_value_is_absent ( ) {
939+ // Both an omitted protobuf value and the schema-only source represent an unset key.
940+ assert ! ( setting_to_config_setting( & AgentConfigSetting {
941+ source: AGENT_DECLARED_ONLY_SOURCE . to_string( ) ,
942+ key: "api_key" . to_string( ) ,
943+ value: None ,
944+ } )
945+ . is_none( ) ) ;
946+
947+ assert ! ( setting_to_config_setting( & agent_setting( AGENT_DECLARED_ONLY_SOURCE , "api_key" , "" ) ) . is_none( ) ) ;
948+ }
949+
950+ #[ test]
951+ fn a_valueless_setting_is_absent_whatever_its_source ( ) {
952+ for source in [ AGENT_DEFAULT_SOURCE , "file" , "remote-config" ] {
953+ assert ! (
954+ setting_to_config_setting( & AgentConfigSetting {
955+ source: source. to_string( ) ,
956+ key: "api_key" . to_string( ) ,
957+ value: Some ( prost_types:: Value {
958+ kind: Some ( Kind :: NullValue ( 0 ) ) ,
959+ } ) ,
960+ } )
961+ . is_none( ) ,
962+ "source {source} with a null value should be absent"
921963 ) ;
922964 }
923965 }
924966
967+ #[ test]
968+ fn an_empty_string_value_is_kept_with_its_provenance ( ) {
969+ // An empty string is still a value; provenance comes from its source, not its content.
970+ let setting =
971+ setting_to_config_setting ( & agent_setting ( "file" , "site" , "" ) ) . expect ( "an empty string is still a value" ) ;
972+
973+ assert_eq ! ( setting. value, Value :: from( "" ) ) ;
974+ assert_eq ! ( setting. provenance, Provenance :: Explicit ) ;
975+
976+ let setting = setting_to_config_setting ( & agent_setting ( AGENT_DEFAULT_SOURCE , "site" , "" ) )
977+ . expect ( "an empty string is still a value" ) ;
978+
979+ assert_eq ! ( setting. value, Value :: from( "" ) ) ;
980+ assert_eq ! ( setting. provenance, Provenance :: Default ) ;
981+ }
982+
925983 #[ test]
926984 fn operator_supplied_sources_are_marked_as_explicit ( ) {
927- // The last source is deliberately not one the Agent publishes today: an unrecognized source is
928- // treated as a real input rather than silently discarded as a default.
985+ // Unknown sources are treated as explicit inputs rather than defaults.
929986 for source in [
930987 "file" ,
931988 "environment-variable" ,
932989 "remote-config" ,
933990 "cli" ,
934991 "source-from-the-future" ,
935992 ] {
936- let setting = setting_to_config_setting ( & agent_setting ( source, "dd_url" , "https://app.datadoghq.eu" ) ) ;
993+ let setting = setting_to_config_setting ( & agent_setting ( source, "dd_url" , "https://app.datadoghq.eu" ) )
994+ . expect ( "an explicit setting has a value" ) ;
937995
938996 assert_eq ! (
939997 setting. provenance,
@@ -944,13 +1002,14 @@ mod tests {
9441002 }
9451003
9461004 #[ test]
947- fn snapshot_settings_keep_their_order_values_and_provenance ( ) {
1005+ fn snapshot_settings_keep_order_values_and_provenance_and_drop_valueless_keys ( ) {
9481006 let snapshot = ConfigSnapshot {
9491007 origin : "core-agent" . to_string ( ) ,
9501008 sequence_id : 1 ,
9511009 settings : vec ! [
9521010 agent_setting( "file" , "site" , "datadoghq.eu" ) ,
9531011 agent_setting( "default" , "dd_url" , "https://app.datadoghq.com" ) ,
1012+ agent_setting( AGENT_DECLARED_ONLY_SOURCE , "api_key" , "" ) ,
9541013 ] ,
9551014 } ;
9561015
0 commit comments