1- use std:: { iter, path:: PathBuf } ;
2-
3- use fs:: file_reader:: { FileReader , FileReaderError } ;
41use serde:: Deserialize ;
5- use serde_yaml:: Sequence ;
6- use thiserror:: Error ;
7- use tracing:: debug;
8-
9- use crate :: config_migrate:: migration:: config:: DirInfo ;
102
113/// Configuration for the infrastructure agent, represented as a raw YAML value.
124#[ derive( Debug , Default , PartialEq , Clone , Deserialize ) ]
@@ -17,20 +9,16 @@ pub struct NewRelicInfraConfig(serde_yaml::Value);
179#[ derive( Debug , Default , PartialEq , Clone , Deserialize ) ]
1810pub struct IntegrationsConfig {
1911 // Integrations must be a list of config items.
20- #[ serde( default ) ]
2112 integrations : Vec < ConfigItem > ,
2213 // We don't perform any validations on the `discovery` key, so we represent it with `Value`.
23- #[ serde( default ) ]
24- discovery : serde_yaml:: Value ,
14+ discovery : Option < serde_yaml:: Value > ,
2515 // We don't perform any validations on the `variables` key, so we represent it with `Value`.
26- #[ serde( default ) ]
27- variables : serde_yaml:: Value ,
16+ variables : Option < serde_yaml:: Value > ,
2817}
2918
3019/// Configuration for the log forwarder, represented as a list of raw YAML mappings.
3120#[ derive( Debug , Default , PartialEq , Clone , Deserialize ) ]
3221pub struct LoggingConfig {
33- #[ serde( default ) ]
3422 logs : Vec < ConfigItem > ,
3523}
3624
@@ -44,133 +32,6 @@ struct ConfigItem {
4432 extra_attrs : serde_yaml:: Mapping ,
4533}
4634
47- /// Wrapper around a supported configuration value that can be merged with other values of the same key.
48- /// The inner value is a tuple where the first element is the key (e.g., "logs", "integrations") and the second element is a sequence of values.
49- /// This structure allows merging multiple configuration entries under the same key by concatenating their sequences.
50- ///
51- /// This is intended to model the configuration files for integrations and logs compatible with the New Relic Infrastructure Agent. For example:
52- ///
53- /// ```yaml
54- /// integrations:
55- /// - name: nri-docker
56- /// when:
57- /// feature: docker_enabled
58- /// file_exists: /var/run/docker.sock
59- /// interval: 15s
60- /// - name: nri-docker
61- /// when:
62- /// feature: docker_enabled
63- /// env_exists:
64- /// FARGATE: "true"
65- /// interval: 15s
66- /// ```
67- #[ derive( Debug , Default , PartialEq , Clone ) ]
68- pub struct SupportedConfigValue ( ( String , Sequence ) ) ;
69-
70- #[ derive( Debug , Error ) ]
71- pub enum SupportedConfigValueError {
72- #[ error( "error reading file `{0}`: `{1}`" ) ]
73- ReadFileError ( PathBuf , FileReaderError ) ,
74-
75- #[ error( "error parsing supported config value from file `{0}`: `{1}`" ) ]
76- ParseError ( PathBuf , serde_yaml:: Error ) ,
77- }
78-
79- impl < ' de > Deserialize < ' de > for SupportedConfigValue {
80- fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
81- where
82- D : serde:: Deserializer < ' de > ,
83- {
84- use serde:: de:: { Error , MapAccess , Visitor } ;
85- use std:: fmt;
86-
87- struct SupportedConfigValueVisitor ;
88-
89- impl < ' de > Visitor < ' de > for SupportedConfigValueVisitor {
90- type Value = SupportedConfigValue ;
91-
92- fn expecting ( & self , formatter : & mut fmt:: Formatter ) -> fmt:: Result {
93- formatter. write_str ( "a map with a single key and a sequence of values" )
94- }
95-
96- fn visit_map < M > ( self , mut map : M ) -> Result < SupportedConfigValue , M :: Error >
97- where
98- M : MapAccess < ' de > ,
99- {
100- if let Some ( ( key, value) ) = map. next_entry :: < String , Sequence > ( ) ? {
101- if map. next_key :: < String > ( ) ?. is_some ( ) {
102- Err ( M :: Error :: custom (
103- "expected a single key-value pair in the map" ,
104- ) )
105- } else {
106- Ok ( SupportedConfigValue ( ( key, value) ) )
107- }
108- } else {
109- Err ( M :: Error :: custom ( "expected a non-empty map" ) )
110- }
111- }
112- }
113-
114- deserializer. deserialize_map ( SupportedConfigValueVisitor )
115- }
116- }
117-
118- impl SupportedConfigValue {
119- /// Attempts to merge another `SupportedConfigValue` into this one.
120- /// Merging is only possible if both values have the same key (the first element of the tuple).
121- /// The key of [`self`] is used as the key of the resulting merged value.
122- /// If the keys match, the sequences (the second element of the tuple) are concatenated.
123- /// If the keys do not match, we return the [`self`] value unchanged.
124- pub fn merge ( mut self , other : Self ) -> Self {
125- if self . 0 . 0 != other. 0 . 0 {
126- debug ! (
127- "cannot merge incompatible config values. Left value key: `{}`, right value key: `{}`" ,
128- self . 0.0 , other. 0.0
129- ) ;
130- } else {
131- self . 0 . 1 . extend ( other. 0 . 1 ) ;
132- }
133- self
134- }
135-
136- fn default_with_key ( key : impl AsRef < str > ) -> Self {
137- SupportedConfigValue ( ( key. as_ref ( ) . to_string ( ) , Sequence :: new ( ) ) )
138- }
139-
140- pub fn from_dir_with_key (
141- file_reader : & impl FileReader ,
142- dir_info : & DirInfo ,
143- key : impl AsRef < str > ,
144- ) -> Result < Self , SupportedConfigValueError > {
145- file_reader
146- . dir_entries ( & dir_info. path )
147- . unwrap_or_default ( )
148- . iter ( )
149- . filter ( |p| dir_info. valid_filename ( p) )
150- . map ( |p| {
151- let file = file_reader
152- . read ( p)
153- . map_err ( |e| SupportedConfigValueError :: ReadFileError ( p. to_path_buf ( ) , e) ) ?;
154- serde_yaml:: from_str :: < SupportedConfigValue > ( & file)
155- . map_err ( |e| SupportedConfigValueError :: ParseError ( p. to_path_buf ( ) , e) )
156- } )
157- . try_fold ( SupportedConfigValue :: default_with_key ( key) , |acc, item| {
158- item. map ( |v| acc. merge ( v) )
159- } )
160- }
161- }
162-
163- impl From < SupportedConfigValue > for serde_yaml:: Value {
164- fn from ( value : SupportedConfigValue ) -> Self {
165- use serde_yaml:: Mapping ;
166- use serde_yaml:: Value :: * ;
167-
168- let k = String ( value. 0 . 0 ) ;
169- let v = Sequence ( value. 0 . 1 ) ;
170- Mapping ( Mapping :: from_iter ( iter:: once ( ( k, v) ) ) )
171- }
172- }
173-
17435#[ cfg( test) ]
17536pub mod tests {
17637 use super :: * ;
@@ -190,6 +51,12 @@ integrations:
19051 env_exists:
19152 FARGATE: "true"
19253 interval: 15s
54+
55+ discovery:
56+ arbitrary_key: arbitrary_value
57+
58+ variables:
59+ arbitrary_key: arbitrary_value
19360"# ;
19461
19562 pub const EXAMPLE_LOGS_CONFIG : & str = r#"
@@ -237,42 +104,38 @@ logs:
237104 pattern: WARN|ERROR
238105"# ;
239106
240- #[ test]
241- fn test_parse_and_merge_integration_config ( ) {
242- let config1: SupportedConfigValue =
243- serde_yaml:: from_str ( EXAMPLE_INTEGRATION_CONFIG ) . unwrap ( ) ;
244- let config2: SupportedConfigValue =
245- serde_yaml:: from_str ( EXAMPLE_INTEGRATION_CONFIG ) . unwrap ( ) ;
246- let merged_config = config1. merge ( config2) ;
247- assert_eq ! ( merged_config. 0.1 . len( ) , 4 ) ;
248- }
107+ // Not testing the parsing of infra agent config, as we
108+ // represent it as an arbitrary YAML value here for simplicity.
249109
250110 #[ test]
251- fn test_parse_and_merge_logs_config ( ) {
252- let config1: SupportedConfigValue = serde_yaml:: from_str ( EXAMPLE_LOGS_CONFIG ) . unwrap ( ) ;
253- let config2: SupportedConfigValue = serde_yaml:: from_str ( EXAMPLE_LOGS_CONFIG ) . unwrap ( ) ;
254- let merged_config = config1. merge ( config2) ;
255- assert_eq ! ( merged_config. 0.1 . len( ) , 12 ) ;
111+ fn serde_logs ( ) {
112+ let config: LoggingConfig = serde_yaml:: from_str ( EXAMPLE_LOGS_CONFIG ) . unwrap ( ) ;
113+ assert_eq ! ( config. logs. len( ) , 6 ) ;
114+ assert_eq ! ( config. logs[ 0 ] . name, "basic-file" ) ;
115+ assert_eq ! ( config. logs[ 1 ] . name, "file-with-spaces-in-path" ) ;
116+ assert_eq ! ( config. logs[ 2 ] . name, "file-with-attributes" ) ;
117+ assert_eq ! ( config. logs[ 3 ] . name, "log-files-in-folder" ) ;
118+ assert_eq ! ( config. logs[ 4 ] . name, "log-file-with-long-lines" ) ;
119+ assert_eq ! ( config. logs[ 5 ] . name, "only-records-with-warn-and-error" ) ;
120+
121+ // no logs key should fail
122+ let err = serde_yaml:: from_str :: < LoggingConfig > ( "" ) . unwrap_err ( ) ;
123+ assert ! ( err. to_string( ) . contains( "missing field `logs`" ) ) ;
256124 }
257125
258126 #[ test]
259- fn test_uncompatible_merge ( ) {
260- let config1: SupportedConfigValue = serde_yaml:: from_str ( EXAMPLE_LOGS_CONFIG ) . unwrap ( ) ;
261- let config2: SupportedConfigValue =
262- serde_yaml:: from_str ( EXAMPLE_INTEGRATION_CONFIG ) . unwrap ( ) ;
263- let result = config1. clone ( ) . merge ( config2) ;
264- assert_eq ! ( result, config1) ;
265- }
266-
267- #[ test]
268- fn bad_serde ( ) {
269- let result: Result < SupportedConfigValue , _ > = serde_yaml:: from_str ( r#"{}"# ) ;
270- assert ! ( result. is_err_and( |e| e. to_string( ) . contains( "expected a non-empty map" ) ) ) ;
271-
272- let result: Result < SupportedConfigValue , _ > = serde_yaml:: from_str ( r#"{"key": "value"}"# ) ;
273- assert ! ( result. is_err_and( |e| { e. to_string( ) . contains( "expected a sequence" ) } ) ) ;
274-
275- let result: Result < SupportedConfigValue , _ > = serde_yaml:: from_str ( r#"{"k1":[],"k2":[]}"# ) ;
276- assert ! ( result. is_err_and( |e| e. to_string( ) . contains( "expected a single key-value pair" ) ) ) ;
127+ fn serde_integrations ( ) {
128+ let config: IntegrationsConfig = serde_yaml:: from_str ( EXAMPLE_INTEGRATION_CONFIG ) . unwrap ( ) ;
129+ assert_eq ! ( config. integrations. len( ) , 2 ) ;
130+ assert_eq ! ( config. integrations[ 0 ] . name, "nri-docker" ) ;
131+ assert_eq ! ( config. integrations[ 1 ] . name, "nri-docker" ) ;
132+
133+ // only integrations key (though empty) should succeed:
134+ let config: IntegrationsConfig = serde_yaml:: from_str ( "integrations: []" ) . unwrap ( ) ;
135+ assert_eq ! ( config. integrations. len( ) , 0 ) ;
136+
137+ // no integrations key should fail
138+ let err = serde_yaml:: from_str :: < IntegrationsConfig > ( "" ) . unwrap_err ( ) ;
139+ assert ! ( err. to_string( ) . contains( "missing field `integrations`" ) ) ;
277140 }
278141}
0 commit comments