core_lib/config/
parser.rs1use 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 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}