Skip to main content

vector/config/
cmd.rs

1use std::path::PathBuf;
2
3use clap::Parser;
4use serde_json::Value;
5
6use super::{ConfigBuilder, load_source_from_paths, loading::ConfigBuilderLoader, process_paths};
7use crate::{cli::handle_config_errors, config};
8
9#[derive(Parser, Debug, Clone)]
10#[command(rename_all = "kebab-case")]
11pub struct Opts {
12    /// Pretty print JSON
13    #[arg(short, long)]
14    pretty: bool,
15
16    /// Include default values where missing from config
17    #[arg(short, long)]
18    include_defaults: bool,
19
20    /// Read configuration from one or more files. Wildcard paths are supported.
21    /// File format is detected from the file name.
22    /// If zero files are specified, the deprecated default config path
23    /// `/etc/vector/vector.yaml` is targeted.
24    #[arg(
25        id = "config",
26        short,
27        long,
28        env = "VECTOR_CONFIG",
29        value_delimiter(',')
30    )]
31    paths: Vec<PathBuf>,
32
33    /// Vector config files in TOML format.
34    #[arg(id = "config-toml", long, value_delimiter(','))]
35    paths_toml: Vec<PathBuf>,
36
37    /// Vector config files in JSON format.
38    #[arg(id = "config-json", long, value_delimiter(','))]
39    paths_json: Vec<PathBuf>,
40
41    /// Vector config files in YAML format.
42    #[arg(id = "config-yaml", long, value_delimiter(','))]
43    paths_yaml: Vec<PathBuf>,
44
45    /// Read configuration from files in one or more directories.
46    /// File format is detected from the file name.
47    ///
48    /// Files not ending in .toml, .json, .yaml, or .yml will be ignored.
49    #[arg(
50        id = "config-dir",
51        short = 'C',
52        long,
53        env = "VECTOR_CONFIG_DIR",
54        value_delimiter(',')
55    )]
56    pub config_dirs: Vec<PathBuf>,
57
58    /// Allow interpolation of environment variables in configuration files. Enabling this may
59    /// expose environment secrets into your Vector configuration.
60    #[arg(
61        long,
62        env = "VECTOR_DANGEROUSLY_ALLOW_ENV_VAR_INTERPOLATION",
63        default_value = "false"
64    )]
65    pub dangerously_allow_env_var_interpolation: bool,
66}
67
68impl Opts {
69    fn paths_with_formats(&self) -> Vec<config::ConfigPath> {
70        config::merge_path_lists(vec![
71            (&self.paths, None),
72            (&self.paths_toml, Some(config::Format::Toml)),
73            (&self.paths_json, Some(config::Format::Json)),
74            (&self.paths_yaml, Some(config::Format::Yaml)),
75        ])
76        .map(|(path, hint)| config::ConfigPath::File(path, hint))
77        .chain(
78            self.config_dirs
79                .iter()
80                .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
81        )
82        .collect()
83    }
84}
85
86/// Helper to merge JSON. Handles objects and array concatenation.
87fn merge_json(a: &mut Value, b: Value) {
88    match (a, b) {
89        (Value::Object(a), Value::Object(b)) => {
90            for (k, v) in b {
91                merge_json(a.entry(k).or_insert(Value::Null), v);
92            }
93        }
94        (a, b) => {
95            *a = b;
96        }
97    }
98}
99
100/// Helper to sort array values.
101fn sort_json_array_values(json: &mut Value) {
102    match json {
103        Value::Array(arr) => {
104            for v in arr.iter_mut() {
105                sort_json_array_values(v);
106            }
107
108            // Since `Value` does not have a native ordering, we first convert
109            // to string, sort, and then convert back to `Value`.
110            //
111            // Practically speaking, there should not be config options that mix
112            // many JSON types in a single array. This is mainly to sort fields
113            // like component inputs.
114            let mut a = arr
115                .iter()
116                .map(|v| serde_json::to_string(v).unwrap())
117                .collect::<Vec<_>>();
118            a.sort();
119            *arr = a
120                .iter()
121                .map(|v| serde_json::from_str(v.as_str()).unwrap())
122                .collect::<Vec<_>>();
123        }
124        Value::Object(json) => {
125            for (_, v) in json {
126                sort_json_array_values(v);
127            }
128        }
129        _ => {}
130    }
131}
132
133/// Convert a raw user config to a JSON string
134fn serialize_to_json(
135    source: toml::value::Table,
136    source_builder: &ConfigBuilder,
137    include_defaults: bool,
138    pretty_print: bool,
139) -> serde_json::Result<String> {
140    // Convert table to JSON
141    let mut source_json = serde_json::to_value(source)
142        .expect("should serialize config source to JSON. Please report.");
143
144    // If a user has requested default fields, we'll serialize a `ConfigBuilder`. Otherwise,
145    // we'll serialize the raw user provided config (without interpolated env vars, to preserve
146    // the original source).
147    if include_defaults {
148        // For security, we don't want environment variables to be interpolated in the final
149        // output, but we *do* want defaults. To work around this, we'll serialize `ConfigBuilder`
150        // to JSON, and merge in the raw config which will contain the pre-interpolated strings.
151        let mut builder = serde_json::to_value(source_builder)
152            .expect("should serialize ConfigBuilder to JSON. Please report.");
153
154        merge_json(&mut builder, source_json);
155
156        source_json = builder
157    }
158
159    sort_json_array_values(&mut source_json);
160
161    // Get a JSON string. This will either be pretty printed or (default) minified.
162    if pretty_print {
163        serde_json::to_string_pretty(&source_json)
164    } else {
165        serde_json::to_string(&source_json)
166    }
167}
168
169/// Function used by the `vector config` subcommand for outputting a normalized configuration.
170/// The purpose of this func is to combine user configuration after processing all paths,
171/// Pipelines expansions, etc. The JSON result of this serialization can itself be used as a config,
172/// which also makes it useful for version control or treating as a singular unit of configuration.
173pub fn cmd(opts: &Opts) -> exitcode::ExitCode {
174    let paths = opts.paths_with_formats();
175    // Start by serializing to a `ConfigBuilder`. This will leverage validation in config
176    // builder fields which we'll use to error out if required.
177    let (paths, builder) = match process_paths(&paths) {
178        Some(paths) => match ConfigBuilderLoader::default().load_from_paths(&paths) {
179            Ok(builder) => (paths, builder),
180            Err(errs) => return handle_config_errors(errs),
181        },
182        None => return exitcode::CONFIG,
183    };
184
185    // Load source TOML.
186    let source = match load_source_from_paths(&paths) {
187        Ok(map) => map,
188        Err(errs) => return handle_config_errors(errs),
189    };
190
191    let json = serialize_to_json(source, &builder, opts.include_defaults, opts.pretty);
192
193    #[allow(clippy::print_stdout)]
194    {
195        println!("{}", json.expect("config should be serializable"));
196    }
197
198    exitcode::OK
199}
200
201#[cfg(all(test, feature = "sources", feature = "transforms", feature = "sinks"))]
202mod tests {
203    use std::collections::HashMap;
204
205    use proptest::{num, prelude::*, sample};
206    use rand::{
207        SeedableRng,
208        prelude::{SliceRandom, StdRng},
209    };
210    use serde_json::json;
211    use similar_asserts::assert_eq;
212    use vector_lib::configurable::component::{
213        SinkDescription, SourceDescription, TransformDescription,
214    };
215
216    use super::merge_json;
217    use crate::{
218        config::{ConfigBuilder, Format, cmd::serialize_to_json, vars},
219        generate,
220        generate::{TransformInputsStrategy, generate_example},
221    };
222
223    #[test]
224    fn test_array_override() {
225        let mut json = json!({
226            "arr": [
227                "value1", "value2"
228            ]
229        });
230
231        let to_override = json!({
232            "arr": [
233                "value3", "value4"
234            ]
235        });
236
237        merge_json(&mut json, to_override);
238
239        assert_eq!(*json.get("arr").unwrap(), json!(["value3", "value4"]))
240    }
241
242    #[test]
243    fn include_defaults_does_not_include_env_vars() {
244        let env_var = "VECTOR_CONFIG_INCLUDE_DEFAULTS_TEST";
245        let env_var_in_arr = "VECTOR_CONFIG_INCLUDE_DEFAULTS_TEST_IN_ARR";
246
247        let config_source = format!(
248            r#"
249            [sources.in]
250            type = "demo_logs"
251            format = "${{{env_var}}}"
252
253            [sinks.out]
254            type = "blackhole"
255            inputs = ["${{{env_var_in_arr}}}"]
256        "#
257        );
258        let interpolated_config_source = vars::interpolate(
259            config_source.as_ref(),
260            &HashMap::from([
261                (env_var.to_string(), "syslog".to_string()),
262                (env_var_in_arr.to_string(), "in".to_string()),
263            ]),
264        )
265        .unwrap();
266
267        let json: serde_json::Value = serde_json::from_str(
268            serialize_to_json(
269                toml::from_str(config_source.as_ref()).unwrap(),
270                &ConfigBuilder::from_toml(interpolated_config_source.as_ref()),
271                true,
272                false,
273            )
274            .unwrap()
275            .as_ref(),
276        )
277        .unwrap();
278
279        assert_eq!(
280            json["sources"]["in"]["format"],
281            json!(format!("${{{}}}", env_var))
282        );
283        assert_eq!(
284            json["sinks"]["out"]["inputs"],
285            json!(vec![format!("${{{}}}", env_var_in_arr)])
286        );
287    }
288
289    /// Select any 2-4 sources
290    fn arb_sources() -> impl Strategy<Value = Vec<&'static str>> {
291        let mut types = SourceDescription::types();
292        // The `file_descriptor` source produces different defaults each time it is used, and so
293        // will never compare equal below.
294        types.retain(|t| *t != "file_descriptor");
295        sample::subsequence(types, 2..=4)
296    }
297
298    /// Select any 2-4 transforms
299    fn arb_transforms() -> impl Strategy<Value = Vec<&'static str>> {
300        sample::subsequence(TransformDescription::types(), 2..=4)
301    }
302
303    /// Select any 2-4 sinks
304    fn arb_sinks() -> impl Strategy<Value = Vec<&'static str>> {
305        sample::subsequence(SinkDescription::types(), 2..=4)
306    }
307
308    fn create_config_source(sources: &[&str], transforms: &[&str], sinks: &[&str]) -> String {
309        // This creates a string in the syntax expected by the `vector generate`
310        // command whose internal mechanics we are using to create valid Vector
311        // configurations.
312        //
313        // Importantly, we have to name the components (in this case, simply by
314        // their type as each type of component is guaranteed to only appear
315        // once), because (in some tests) we'd like to shuffle the configuration
316        // later in a way that does not change its actual semantics. Otherwise,
317        // an autogenerated ID like `source0` could correspond to different
318        // sources depending on the ordering of the `vector generate` input.
319        //
320        // We also append a fixed `remap` transform to the transforms list. This
321        // ensures sink inputs are consistent since `generate` uses the last
322        // transform the input for each sink.
323        let generate_config_str = format!(
324            "{}/{}/{}",
325            sources
326                .iter()
327                .map(|source| format!("{source}:{source}"))
328                .collect::<Vec<_>>()
329                .join(","),
330            transforms
331                .iter()
332                .map(|transform| format!("{transform}:{transform}"))
333                .chain(vec!["manually-added-remap:remap".to_string()])
334                .collect::<Vec<_>>()
335                .join(","),
336            sinks
337                .iter()
338                .map(|sink| format!("{sink}:{sink}"))
339                .collect::<Vec<_>>()
340                .join(","),
341        );
342        let opts = generate::Opts {
343            fragment: true,
344            expression: generate_config_str.to_string(),
345            file: None,
346            format: Format::Toml,
347        };
348        generate_example(&opts, TransformInputsStrategy::All).expect("invalid config generated")
349    }
350
351    proptest! {
352        #[test]
353        /// Output should be the same regardless of input config ordering
354        fn output_has_consistent_ordering(mut sources in arb_sources(), mut transforms in arb_transforms(), mut sinks in arb_sinks(), seed in num::u64::ANY) {
355            let config_source = create_config_source(sources.as_ref(), transforms.as_ref(), sinks.as_ref());
356
357            // Shuffle the ordering of components which shuffles the order in
358            // which items appear in the TOML config
359            let mut rng = StdRng::seed_from_u64(seed);
360            sources.shuffle(&mut rng);
361            transforms.shuffle(&mut rng);
362            sinks.shuffle(&mut rng);
363            let shuffled_config_source = create_config_source(sources.as_ref(), transforms.as_ref(), sinks.as_ref());
364
365            let json = serialize_to_json(
366                toml::from_str(config_source.as_ref()).unwrap(),
367                &ConfigBuilder::from_toml(config_source.as_ref()),
368                false,
369                false
370            )
371            .unwrap();
372            let shuffled_json = serialize_to_json(
373                toml::from_str(shuffled_config_source.as_ref()).unwrap(),
374                &ConfigBuilder::from_toml(shuffled_config_source.as_ref()),
375                false,
376                false
377            )
378            .unwrap();
379
380            assert_eq!(json, shuffled_json);
381        }
382    }
383
384    proptest! {
385        #[test]
386        /// Output is a valid configuration
387        fn output_is_a_valid_config(sources in arb_sources(), transforms in arb_transforms(), sinks in arb_sinks()) {
388            let config_source = create_config_source(sources.as_ref(), transforms.as_ref(), sinks.as_ref());
389            let json = serialize_to_json(
390                toml::from_str(config_source.as_ref()).unwrap(),
391                &ConfigBuilder::from_toml(config_source.as_ref()),
392                false,
393                false
394            )
395            .unwrap();
396            assert!(serde_json::from_str::<ConfigBuilder>(json.as_ref()).is_ok());
397        }
398    }
399}