core_lib/config/
parser.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fs,
4    io::Write,
5    path::Path,
6};
7
8use anyhow::{Context, Result};
9
10use crate::{
11    config::{Job, ProjectConfig, stdin_is_tty},
12    log::logger::{LogLevel, Logger},
13};
14
15pub fn check_dependency_graph(config: &ProjectConfig) -> Result<()> {
16    let pipeline = &config.pipeline;
17
18    for (name, job) in pipeline.jobs.iter() {
19        if job.needs.contains(name) {
20            return Err(anyhow::anyhow!("Job '{}' cannot depend on itself", name));
21        }
22        for dep in &job.needs {
23            if !pipeline.jobs.contains_key(dep) {
24                return Err(anyhow::anyhow!(
25                    "Job '{}' depends on unknown job '{}'",
26                    name,
27                    dep
28                ));
29            }
30        }
31    }
32
33    fn visit(
34        name: &str,
35        pipeline: &HashMap<String, Job>,
36        temp: &mut HashSet<String>,
37        perm: &mut HashSet<String>,
38        path: &mut Vec<String>,
39    ) -> anyhow::Result<()> {
40        if perm.contains(name) {
41            return Ok(());
42        }
43        if !temp.insert(name.to_string()) {
44            let cycle_start_index: usize = path.iter().position(|n| n == name).unwrap_or(0);
45            let cycle_path: Vec<_> = path[cycle_start_index..]
46                .iter()
47                .chain(std::iter::once(&name.to_string()))
48                .cloned()
49                .collect();
50            return Err(anyhow::anyhow!(
51                "Cycle detected: [{}]",
52                cycle_path.join(" -> ")
53            ));
54        }
55
56        path.push(name.to_string());
57        if let Some(job) = pipeline.get(name) {
58            for dep in &job.needs {
59                visit(dep, pipeline, temp, perm, path)?;
60            }
61        }
62        temp.remove(name);
63        perm.insert(name.to_string());
64        path.pop();
65        Ok(())
66    }
67    let mut temp = HashSet::new();
68    let mut perm = HashSet::new();
69    let mut path = Vec::new();
70    for name in pipeline.jobs.keys() {
71        visit(name, &pipeline.jobs, &mut temp, &mut perm, &mut path)?;
72    }
73    Ok(())
74}
75
76pub fn load_config(path: &Path) -> Result<ProjectConfig> {
77    let content: String =
78        fs::read_to_string(path).with_context(|| format!("Error reading config file {path:?}"))?;
79
80    let mut config: ProjectConfig =
81        serde_yaml::from_str(&content).with_context(|| "Error parsing YAML configuration file")?;
82
83    let mut skipped_missing_variables = HashSet::new();
84
85    // resolve secret env variable for each job
86    for (job_name, job) in config.pipeline.jobs.iter_mut() {
87        let env_map = job.env.as_mut();
88        if env_map.is_none() {
89            continue;
90        }
91
92        for (name, value) in env_map.unwrap().iter_mut() {
93            if !value.starts_with("$") {
94                continue;
95            }
96
97            let env_key = if !&value[1..].is_empty() {
98                &value[1..]
99            } else {
100                name
101            };
102
103            let extraction_result = std::env::var(env_key);
104            if let Ok(env_value) = extraction_result {
105                *value = env_value;
106                continue;
107            }
108
109            Logger::write(
110                &format!(r#""${}" not found for job "{job_name}""#, env_key),
111                LogLevel::Warning,
112            );
113
114            if skipped_missing_variables.contains(env_key) {
115                *value = "".to_string();
116                continue;
117            }
118
119            if stdin_is_tty() {
120                if ask_continue_anyway()? {
121                    skipped_missing_variables.insert(env_key.to_string());
122                    *value = "".to_string();
123                    continue;
124                } else {
125                    return Err(extraction_result.unwrap_err().into());
126                }
127            } else {
128                return Err(anyhow::anyhow!(
129                    "Missing env variable '{}' and no TTY to ask user",
130                    env_key
131                ));
132            }
133        }
134    }
135    // dbg!(&config);
136    check_dependency_graph(&config)?;
137    Ok(config)
138}
139
140fn ask_continue_anyway() -> Result<bool> {
141    loop {
142        eprint!("Continue anyway ? [y/N] ");
143        std::io::stdout().flush()?;
144
145        let mut buffer = String::new();
146        std::io::stdin().read_line(&mut buffer)?;
147
148        let input = buffer.chars().next();
149
150        match input {
151            Some('y' | 'Y') => return Ok(true),
152            Some('n' | 'N') => return Ok(false),
153
154            _ => {
155                eprintln!("Invalid input, please retry.");
156                continue;
157            }
158        }
159    }
160}