Skip to main content

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