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 #[arg(short, long)]
14 pretty: bool,
15
16 #[arg(short, long)]
18 include_defaults: bool,
19
20 #[arg(
25 id = "config",
26 short,
27 long,
28 env = "VECTOR_CONFIG",
29 value_delimiter(',')
30 )]
31 paths: Vec<PathBuf>,
32
33 #[arg(id = "config-toml", long, value_delimiter(','))]
35 paths_toml: Vec<PathBuf>,
36
37 #[arg(id = "config-json", long, value_delimiter(','))]
39 paths_json: Vec<PathBuf>,
40
41 #[arg(id = "config-yaml", long, value_delimiter(','))]
43 paths_yaml: Vec<PathBuf>,
44
45 #[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 #[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
86fn 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
100fn 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 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
133fn 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 let mut source_json = serde_json::to_value(source)
142 .expect("should serialize config source to JSON. Please report.");
143
144 if include_defaults {
148 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 if pretty_print {
163 serde_json::to_string_pretty(&source_json)
164 } else {
165 serde_json::to_string(&source_json)
166 }
167}
168
169pub fn cmd(opts: &Opts) -> exitcode::ExitCode {
174 let paths = opts.paths_with_formats();
175 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 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 fn arb_sources() -> impl Strategy<Value = Vec<&'static str>> {
291 let mut types = SourceDescription::types();
292 types.retain(|t| *t != "file_descriptor");
295 sample::subsequence(types, 2..=4)
296 }
297
298 fn arb_transforms() -> impl Strategy<Value = Vec<&'static str>> {
300 sample::subsequence(TransformDescription::types(), 2..=4)
301 }
302
303 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 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 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 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 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}