Skip to main content

greentic_setup/cli_helpers/
env_vars.rs

1//! Environment variable placeholder handling.
2//!
3//! Functions for collecting, resolving, and applying environment variable
4//! placeholders in setup answers (e.g., `${PUBLIC_BASE_URL}`).
5
6use std::collections::{BTreeMap, HashMap};
7use std::io::{self, Write as _};
8
9use anyhow::{Result, bail};
10
11use crate::engine::LoadedAnswers;
12
13/// Represents an environment variable placeholder found in answers.
14#[derive(Debug, Clone)]
15pub struct EnvVarPlaceholder {
16    /// The placeholder string (e.g., "${PUBLIC_BASE_URL}")
17    pub placeholder: String,
18    /// The environment variable name (e.g., "PUBLIC_BASE_URL")
19    pub var_name: String,
20    /// The resolved value from environment, if available
21    pub resolved_value: Option<String>,
22    /// Which providers use this placeholder
23    pub used_by: Vec<String>,
24}
25
26/// Collect all environment variable placeholders from loaded answers.
27pub fn collect_env_var_placeholders(loaded: &LoadedAnswers) -> Vec<EnvVarPlaceholder> {
28    let mut placeholders: BTreeMap<String, EnvVarPlaceholder> = BTreeMap::new();
29
30    // Check platform_setup.static_routes.public_base_url
31    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    // Check each provider's answers
47    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
74/// Extract environment variable name from a placeholder like "${VAR_NAME}".
75fn 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
83/// Display environment variable placeholders and prompt for missing values.
84///
85/// Returns a map of env var name -> resolved value (either from env or user input).
86/// Returns `Err` if user cancels.
87pub 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                // Mask sensitive values (tokens, passwords, secrets)
108                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    // Prompt for missing values
129    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                // Use secure password input for sensitive values
139                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
165/// Apply resolved environment variable values to loaded answers.
166///
167/// Replaces `${VAR_NAME}` placeholders with actual values from the resolved map.
168pub fn apply_resolved_env_vars(loaded: &mut LoadedAnswers, resolved: &HashMap<String, String>) {
169    // Apply to platform_setup.static_routes.public_base_url
170    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    // Apply to each provider's answers
179    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
193/// Check if a variable name suggests sensitive data.
194fn 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
203/// Mask a sensitive value, showing only first and last 4 characters.
204fn 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}