greentic_setup/cli_helpers/
env_vars.rs1use std::collections::{BTreeMap, HashMap};
7use std::io::{self, Write as _};
8
9use anyhow::{Result, bail};
10
11use crate::engine::LoadedAnswers;
12
13#[derive(Debug, Clone)]
15pub struct EnvVarPlaceholder {
16 pub placeholder: String,
18 pub var_name: String,
20 pub resolved_value: Option<String>,
22 pub used_by: Vec<String>,
24}
25
26pub fn collect_env_var_placeholders(loaded: &LoadedAnswers) -> Vec<EnvVarPlaceholder> {
28 let mut placeholders: BTreeMap<String, EnvVarPlaceholder> = BTreeMap::new();
29
30 if let Some(ref routes) = loaded.platform_setup.static_routes
32 && let Some(ref value) = routes.public_base_url
33 && let Some(var_name) = extract_env_var_name(value)
34 {
35 let entry = placeholders
36 .entry(var_name.clone())
37 .or_insert_with(|| EnvVarPlaceholder {
38 placeholder: value.to_string(),
39 var_name: var_name.clone(),
40 resolved_value: std::env::var(&var_name).ok(),
41 used_by: Vec::new(),
42 });
43 entry.used_by.push("platform_setup".to_string());
44 }
45
46 for (provider_id, answers) in &loaded.setup_answers {
48 if let Some(obj) = answers.as_object() {
49 for (key, value) in obj {
50 if let Some(s) = value.as_str()
51 && let Some(var_name) = extract_env_var_name(s)
52 {
53 let entry =
54 placeholders
55 .entry(var_name.clone())
56 .or_insert_with(|| EnvVarPlaceholder {
57 placeholder: s.to_string(),
58 var_name: var_name.clone(),
59 resolved_value: std::env::var(&var_name).ok(),
60 used_by: Vec::new(),
61 });
62 let provider_key = format!("{provider_id}.{key}");
63 if !entry.used_by.contains(&provider_key) {
64 entry.used_by.push(provider_key);
65 }
66 }
67 }
68 }
69 }
70
71 placeholders.into_values().collect()
72}
73
74fn extract_env_var_name(value: &str) -> Option<String> {
76 if value.starts_with("${") && value.ends_with('}') {
77 Some(value[2..value.len() - 1].to_string())
78 } else {
79 None
80 }
81}
82
83pub fn confirm_env_var_placeholders(
88 placeholders: &[EnvVarPlaceholder],
89) -> Result<HashMap<String, String>> {
90 use rpassword::prompt_password;
91
92 let mut resolved: HashMap<String, String> = HashMap::new();
93
94 if placeholders.is_empty() {
95 return Ok(resolved);
96 }
97
98 println!();
99 println!("── Environment Variables ──");
100 println!("The following environment variables will be used:\n");
101
102 let mut missing: Vec<&EnvVarPlaceholder> = Vec::new();
103
104 for placeholder in placeholders {
105 match &placeholder.resolved_value {
106 Some(value) => {
107 let display_value = if is_sensitive_var(&placeholder.var_name) {
109 mask_value(value)
110 } else {
111 value.clone()
112 };
113 println!(
114 " ${:<30} \x1b[32m✓\x1b[0m {}",
115 placeholder.var_name, display_value
116 );
117 resolved.insert(placeholder.var_name.clone(), value.clone());
118 }
119 None => {
120 println!(" ${:<30} \x1b[31m✗ NOT SET\x1b[0m", placeholder.var_name);
121 missing.push(placeholder);
122 }
123 };
124 }
125
126 println!();
127
128 if !missing.is_empty() {
130 println!("Enter values for missing environment variables:");
131 println!("(Press Enter to skip and keep placeholder, or 'q' to cancel)\n");
132
133 for placeholder in missing {
134 let is_sensitive = is_sensitive_var(&placeholder.var_name);
135 let prompt = format!(" ${}: ", placeholder.var_name);
136
137 let input = if is_sensitive {
138 print!("{}", prompt);
140 io::stdout().flush()?;
141 prompt_password("").unwrap_or_default()
142 } else {
143 print!("{}", prompt);
144 io::stdout().flush()?;
145 let mut buf = String::new();
146 io::stdin().read_line(&mut buf)?;
147 buf.trim().to_string()
148 };
149
150 if input.eq_ignore_ascii_case("q") {
151 bail!("Setup cancelled by user");
152 }
153
154 if !input.is_empty() {
155 resolved.insert(placeholder.var_name.clone(), input);
156 }
157 }
158
159 println!();
160 }
161
162 Ok(resolved)
163}
164
165pub fn apply_resolved_env_vars(loaded: &mut LoadedAnswers, resolved: &HashMap<String, String>) {
169 if let Some(ref mut routes) = loaded.platform_setup.static_routes
171 && let Some(ref mut value) = routes.public_base_url
172 && let Some(var_name) = extract_env_var_name(value)
173 && let Some(resolved_value) = resolved.get(&var_name)
174 {
175 *value = resolved_value.clone();
176 }
177
178 for (_provider_id, answers) in loaded.setup_answers.iter_mut() {
180 if let Some(obj) = answers.as_object_mut() {
181 for (_key, value) in obj.iter_mut() {
182 if let Some(s) = value.as_str()
183 && let Some(var_name) = extract_env_var_name(s)
184 && let Some(resolved_value) = resolved.get(&var_name)
185 {
186 *value = serde_json::Value::String(resolved_value.clone());
187 }
188 }
189 }
190 }
191}
192
193fn is_sensitive_var(name: &str) -> bool {
195 let lower = name.to_lowercase();
196 lower.contains("token")
197 || lower.contains("password")
198 || lower.contains("secret")
199 || lower.contains("key")
200 || lower.contains("credential")
201}
202
203fn mask_value(value: &str) -> String {
205 if value.len() <= 12 {
206 "*".repeat(value.len())
207 } else {
208 format!("{}...{}", &value[..4], &value[value.len() - 4..])
209 }
210}