vector/
convert_config.rs

1use crate::config::{format, ConfigBuilder, Format};
2use clap::Parser;
3use colored::*;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8#[derive(Parser, Debug)]
9#[command(rename_all = "kebab-case")]
10pub struct Opts {
11    /// The input path. It can be a single file or a directory. If this points to a directory,
12    /// all files with a "toml", "yaml" or "json" extension will be converted.
13    pub(crate) input_path: PathBuf,
14
15    /// The output file or directory to be created. This command will fail if the output directory exists.
16    pub(crate) output_path: PathBuf,
17
18    /// The target format to which existing config files will be converted to.
19    #[arg(long, default_value = "yaml")]
20    pub(crate) output_format: Format,
21}
22
23fn check_paths(opts: &Opts) -> Result<(), String> {
24    let in_metadata = fs::metadata(&opts.input_path)
25        .unwrap_or_else(|_| panic!("Failed to get metadata for: {:?}", &opts.input_path));
26
27    if opts.output_path.exists() {
28        return Err(format!(
29            "Output path {:?} already exists. Please provide a non-existing output path.",
30            opts.output_path
31        ));
32    }
33
34    if opts.output_path.extension().is_none() {
35        if in_metadata.is_file() {
36            return Err(format!(
37                "{:?} points to a file but {:?} points to a directory.",
38                opts.input_path, opts.output_path
39            ));
40        }
41    } else if in_metadata.is_dir() {
42        return Err(format!(
43            "{:?} points to a directory but {:?} points to a file.",
44            opts.input_path, opts.output_path
45        ));
46    }
47
48    Ok(())
49}
50
51pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode {
52    if let Err(e) = check_paths(opts) {
53        #[allow(clippy::print_stderr)]
54        {
55            eprintln!("{}", e.red());
56        }
57        return exitcode::SOFTWARE;
58    }
59
60    if opts.input_path.is_file() && opts.output_path.extension().is_some() {
61        if let Some(base_dir) = opts.output_path.parent() {
62            if !base_dir.exists() {
63                fs::create_dir_all(base_dir).unwrap_or_else(|_| {
64                    panic!("Failed to create output dir(s): {:?}", &opts.output_path)
65                });
66            }
67        }
68
69        match convert_config(&opts.input_path, &opts.output_path, opts.output_format) {
70            Ok(_) => exitcode::OK,
71            Err(errors) => {
72                #[allow(clippy::print_stderr)]
73                {
74                    errors.iter().for_each(|e| eprintln!("{}", e.red()));
75                }
76                exitcode::SOFTWARE
77            }
78        }
79    } else {
80        match walk_dir_and_convert(&opts.input_path, &opts.output_path, opts.output_format) {
81            Ok(()) => {
82                #[allow(clippy::print_stdout)]
83                {
84                    println!(
85                        "Finished conversion(s). Results are in {:?}",
86                        opts.output_path
87                    );
88                }
89                exitcode::OK
90            }
91            Err(errors) => {
92                #[allow(clippy::print_stderr)]
93                {
94                    errors.iter().for_each(|e| eprintln!("{}", e.red()));
95                }
96                exitcode::SOFTWARE
97            }
98        }
99    }
100}
101
102fn convert_config(
103    input_path: &Path,
104    output_path: &Path,
105    output_format: Format,
106) -> Result<(), Vec<String>> {
107    if output_path.exists() {
108        return Err(vec![format!("Output path {output_path:?} exists")]);
109    }
110    let input_format = match Format::from_str(
111        input_path
112            .extension()
113            .unwrap_or_else(|| panic!("Failed to get extension for: {input_path:?}"))
114            .to_str()
115            .unwrap_or_else(|| panic!("Failed to convert OsStr to &str for: {input_path:?}")),
116    ) {
117        Ok(format) => format,
118        Err(_) => return Ok(()), // skip irrelevant files
119    };
120
121    if input_format == output_format {
122        return Ok(());
123    }
124
125    #[allow(clippy::print_stdout)]
126    {
127        println!("Converting {input_path:?} config to {output_format:?}.");
128    }
129    let file_contents = fs::read_to_string(input_path).map_err(|e| vec![e.to_string()])?;
130    let builder: ConfigBuilder = format::deserialize(&file_contents, input_format)?;
131    let config = builder.build()?;
132    let output_string =
133        format::serialize(&config, output_format).map_err(|e| vec![e.to_string()])?;
134    fs::write(output_path, output_string).map_err(|e| vec![e.to_string()])?;
135
136    #[allow(clippy::print_stdout)]
137    {
138        println!("Wrote result to {output_path:?}.");
139    }
140    Ok(())
141}
142
143fn walk_dir_and_convert(
144    input_path: &Path,
145    output_dir: &Path,
146    output_format: Format,
147) -> Result<(), Vec<String>> {
148    let mut errors = Vec::new();
149
150    if input_path.is_dir() {
151        for entry in fs::read_dir(input_path)
152            .unwrap_or_else(|_| panic!("Failed to read dir: {input_path:?}"))
153        {
154            let entry_path = entry
155                .unwrap_or_else(|_| panic!("Failed to get entry for dir: {input_path:?}"))
156                .path();
157            let new_output_dir = if entry_path.is_dir() {
158                let last_component = entry_path
159                    .file_name()
160                    .unwrap_or_else(|| panic!("Failed to get file_name for {entry_path:?}"));
161                let new_dir = output_dir.join(last_component);
162
163                if !new_dir.exists() {
164                    fs::create_dir_all(&new_dir)
165                        .unwrap_or_else(|_| panic!("Failed to create output dir: {new_dir:?}"));
166                }
167                new_dir
168            } else {
169                output_dir.to_path_buf()
170            };
171
172            if let Err(new_errors) = walk_dir_and_convert(
173                &input_path.join(&entry_path),
174                &new_output_dir,
175                output_format,
176            ) {
177                errors.extend(new_errors);
178            }
179        }
180    } else {
181        let output_path = output_dir.join(
182            input_path
183                .with_extension(output_format.to_string().as_str())
184                .file_name()
185                .ok_or_else(|| {
186                    vec![format!(
187                        "Cannot create output path for input: {input_path:?}"
188                    )]
189                })?,
190        );
191        if let Err(new_errors) = convert_config(input_path, &output_path, output_format) {
192            errors.extend(new_errors);
193        }
194    }
195
196    if errors.is_empty() {
197        Ok(())
198    } else {
199        Err(errors)
200    }
201}
202
203#[cfg(all(
204    test,
205    feature = "sources-demo_logs",
206    feature = "transforms-remap",
207    feature = "sinks-console"
208))]
209mod tests {
210    use crate::config::{format, ConfigBuilder, Format};
211    use crate::convert_config::{check_paths, walk_dir_and_convert, Opts};
212    use std::path::{Path, PathBuf};
213    use std::str::FromStr;
214    use std::{env, fs};
215    use tempfile::tempdir;
216
217    fn test_data_dir() -> PathBuf {
218        PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("tests/data/cmd/config")
219    }
220
221    // Read the contents of the specified `path` and deserialize them into a `ConfigBuilder`.
222    // Finally serialize them a string again. Configs do not implement equality,
223    // so for these tests we will rely on strings for comparisons.
224    fn convert_file_to_config_string(path: &Path) -> String {
225        let files_contents = fs::read_to_string(path).unwrap();
226        let extension = path.extension().unwrap().to_str().unwrap();
227        let file_format = Format::from_str(extension).unwrap();
228        let builder: ConfigBuilder = format::deserialize(&files_contents, file_format).unwrap();
229        let config = builder.build().unwrap();
230
231        format::serialize(&config, file_format).unwrap()
232    }
233
234    #[test]
235    fn invalid_path_opts() {
236        let check_error = |opts, pattern| {
237            let error = check_paths(&opts).unwrap_err();
238            assert!(error.contains(pattern));
239        };
240
241        check_error(
242            Opts {
243                input_path: ["./"].iter().collect(),
244                output_path: ["./"].iter().collect(),
245                output_format: Format::Yaml,
246            },
247            "already exists",
248        );
249
250        check_error(
251            Opts {
252                input_path: ["./"].iter().collect(),
253                output_path: ["./out.yaml"].iter().collect(),
254                output_format: Format::Yaml,
255            },
256            "points to a file.",
257        );
258
259        check_error(
260            Opts {
261                input_path: [test_data_dir(), "config_2.toml".into()].iter().collect(),
262                output_path: ["./another_dir"].iter().collect(),
263                output_format: Format::Yaml,
264            },
265            "points to a directory.",
266        );
267    }
268
269    #[test]
270    fn convert_all_from_dir() {
271        let input_path = test_data_dir();
272        let output_dir = tempdir()
273            .expect("Unable to create tempdir for config")
274            .keep();
275        walk_dir_and_convert(&input_path, &output_dir, Format::Yaml).unwrap();
276
277        let mut count: usize = 0;
278        let original_config = convert_file_to_config_string(&test_data_dir().join("config_1.yaml"));
279        for entry in fs::read_dir(&output_dir).unwrap() {
280            let entry = entry.unwrap();
281            let path = entry.path();
282            if path.is_file() {
283                let extension = path.extension().unwrap().to_str().unwrap();
284                if extension == Format::Yaml.to_string() {
285                    // Note that here we read the converted string directly.
286                    let converted_config = fs::read_to_string(output_dir.join(&path)).unwrap();
287                    assert_eq!(converted_config, original_config);
288                    count += 1;
289                }
290            }
291        }
292        // There two non-yaml configs in the input directory.
293        assert_eq!(count, 2);
294    }
295}