Skip to main content

systemprompt_config/services/
manager.rs

1use super::types::{DeployEnvironment, EnvironmentConfig};
2use super::writer::ConfigWriter;
3use anyhow::{anyhow, Result};
4use regex::Regex;
5use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8use systemprompt_logging::CliService;
9
10#[derive(Debug)]
11pub struct ConfigManager {
12    project_root: PathBuf,
13    environments_dir: PathBuf,
14    writer: ConfigWriter,
15}
16
17impl ConfigManager {
18    pub fn new(project_root: PathBuf) -> Self {
19        let environments_dir = project_root.join("infrastructure/environments");
20        let writer = ConfigWriter::new(project_root.clone());
21        Self {
22            project_root,
23            environments_dir,
24            writer,
25        }
26    }
27
28    pub fn generate_config(&self, environment: DeployEnvironment) -> Result<EnvironmentConfig> {
29        CliService::info(&format!(
30            "Building configuration for environment: {}",
31            environment.as_str()
32        ));
33
34        let base_config_path = self.environments_dir.join("base.yaml");
35        let env_config_path = self
36            .environments_dir
37            .join(environment.as_str())
38            .join("config.yaml");
39
40        if !base_config_path.exists() {
41            return Err(anyhow!(
42                "Base config not found: {}",
43                base_config_path.display()
44            ));
45        }
46
47        if !env_config_path.exists() {
48            return Err(anyhow!(
49                "Environment config not found: {}",
50                env_config_path.display()
51            ));
52        }
53
54        self.load_secrets()?;
55
56        CliService::success(&format!(
57            "   Parsing base config: {}",
58            base_config_path.display()
59        ));
60        let base_vars = Self::yaml_to_flat_map(&base_config_path)?;
61
62        CliService::success(&format!(
63            "   Parsing environment config: {}",
64            env_config_path.display()
65        ));
66        let env_vars = Self::yaml_to_flat_map(&env_config_path)?;
67
68        let merged = Self::merge_configs(base_vars, env_vars);
69
70        let resolved = Self::resolve_variables(merged)?;
71
72        CliService::success("   Configuration generated successfully");
73
74        Ok(EnvironmentConfig {
75            environment,
76            variables: resolved,
77        })
78    }
79
80    fn load_secrets(&self) -> Result<()> {
81        let secrets_file = self.project_root.join(".env.secrets");
82
83        if secrets_file.exists() {
84            CliService::info(&format!(
85                "   Loading secrets from: {}",
86                secrets_file.display()
87            ));
88            let content = fs::read_to_string(&secrets_file)?;
89
90            for line in content.lines() {
91                let line = line.trim();
92                if line.is_empty() || line.starts_with('#') {
93                    continue;
94                }
95
96                if let Some((key, value)) = line.split_once('=') {
97                    std::env::set_var(key.trim(), value.trim().trim_matches('"'));
98                }
99            }
100
101            CliService::success("   Secrets loaded");
102        } else {
103            CliService::warning("   No .env.secrets file found");
104        }
105
106        Ok(())
107    }
108
109    fn yaml_to_flat_map(yaml_path: &Path) -> Result<HashMap<String, String>> {
110        let content = fs::read_to_string(yaml_path)?;
111        let yaml: serde_yaml::Value = serde_yaml::from_str(&content)?;
112
113        let mut flat_map = HashMap::new();
114        Self::flatten_yaml(&yaml, String::new(), &mut flat_map);
115
116        Ok(flat_map)
117    }
118
119    fn flatten_yaml(
120        value: &serde_yaml::Value,
121        prefix: String,
122        result: &mut HashMap<String, String>,
123    ) {
124        match value {
125            serde_yaml::Value::Mapping(map) => {
126                for (k, v) in map {
127                    if let Some(key_str) = k.as_str() {
128                        let new_prefix = if prefix.is_empty() {
129                            key_str.to_uppercase()
130                        } else {
131                            format!("{}_{}", prefix, key_str.to_uppercase())
132                        };
133                        Self::flatten_yaml(v, new_prefix, result);
134                    }
135                }
136            },
137            serde_yaml::Value::Sequence(_) => {
138                tracing::warn!(key = %prefix, "YAML sequences are not supported in config flattening - skipping");
139            },
140            _ => {
141                if let Some(str_val) = value.as_str() {
142                    result.insert(prefix, str_val.to_string());
143                } else if let Some(num_val) = value.as_i64() {
144                    result.insert(prefix, num_val.to_string());
145                } else if let Some(bool_val) = value.as_bool() {
146                    result.insert(prefix, bool_val.to_string());
147                } else if let Some(float_val) = value.as_f64() {
148                    result.insert(prefix, float_val.to_string());
149                }
150            },
151        }
152    }
153
154    fn merge_configs(
155        base: HashMap<String, String>,
156        env: HashMap<String, String>,
157    ) -> HashMap<String, String> {
158        let mut merged = base;
159        for (k, v) in env {
160            merged.insert(k, v);
161        }
162        merged
163    }
164
165    fn resolve_variables(mut vars: HashMap<String, String>) -> Result<HashMap<String, String>> {
166        let var_regex = Regex::new(r"\$\{([^}:]+)(?::-(.*?))?\}")?;
167        let max_passes = 5;
168
169        for current_pass in 0..max_passes {
170            let mut changes_made = false;
171
172            for (key, value) in vars.clone() {
173                if var_regex.is_match(&value) {
174                    let resolved = Self::resolve_value(&value, &vars, &var_regex)?;
175
176                    if resolved != value {
177                        vars.insert(key, resolved);
178                        changes_made = true;
179                    }
180                }
181            }
182
183            if !changes_made {
184                break;
185            }
186
187            if current_pass == max_passes - 1 && changes_made {
188                let unresolved: Vec<_> = vars
189                    .iter()
190                    .filter(|(_, v)| var_regex.is_match(v))
191                    .map(|(k, v)| format!("{k} = {v}"))
192                    .collect();
193
194                if !unresolved.is_empty() {
195                    return Err(anyhow!(
196                        "Failed to resolve after {} passes:\n{}",
197                        max_passes,
198                        unresolved.join("\n")
199                    ));
200                }
201            }
202        }
203
204        Ok(vars)
205    }
206
207    fn resolve_value(
208        value: &str,
209        vars: &HashMap<String, String>,
210        var_regex: &Regex,
211    ) -> Result<String> {
212        let mut result = value.to_string();
213
214        for cap in var_regex.captures_iter(value) {
215            let full_match = cap
216                .get(0)
217                .ok_or_else(|| anyhow!("Regex capture group 0 missing"))?
218                .as_str();
219            let var_name = cap
220                .get(1)
221                .ok_or_else(|| anyhow!("Regex capture group 1 missing"))?
222                .as_str();
223            let default_value = cap.get(2).map(|m| m.as_str());
224
225            let replacement = std::env::var(var_name).unwrap_or_else(|_| {
226                vars.get(var_name).cloned().unwrap_or_else(|| {
227                    default_value.map_or_else(|| full_match.to_string(), ToString::to_string)
228                })
229            });
230
231            result = result.replace(full_match, &replacement);
232        }
233
234        Ok(result)
235    }
236
237    pub fn write_env_file(config: &EnvironmentConfig, output_path: &Path) -> Result<()> {
238        ConfigWriter::write_env_file(config, output_path)
239    }
240
241    pub fn write_web_env_file(&self, config: &EnvironmentConfig) -> Result<()> {
242        self.writer.write_web_env_file(config)
243    }
244}