vector/
convert_config.rs

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