cuenv_core/
environment.rs

1//! Environment management for cuenv
2//!
3//! This module handles environment variables from CUE configurations,
4//! including extraction, propagation, and environment-specific overrides.
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::env;
10
11/// Policy for controlling environment variable access
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
13pub struct Policy {
14    /// Allowlist of task names that can access this variable
15    #[serde(skip_serializing_if = "Option::is_none", rename = "allowTasks")]
16    pub allow_tasks: Option<Vec<String>>,
17
18    /// Allowlist of exec commands that can access this variable
19    #[serde(skip_serializing_if = "Option::is_none", rename = "allowExec")]
20    pub allow_exec: Option<Vec<String>>,
21}
22
23/// Environment variable with optional access policies
24#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
25pub struct EnvVarWithPolicies {
26    /// The actual value
27    pub value: EnvValueSimple,
28
29    /// Optional access policies
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub policies: Option<Vec<Policy>>,
32}
33
34/// Simple environment variable values (non-recursive)
35#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
36#[serde(untagged)]
37pub enum EnvValueSimple {
38    String(String),
39    Int(i64),
40    Bool(bool),
41    Secret(crate::secrets::Secret),
42}
43
44/// Environment variable values can be strings, integers, booleans, secrets, or values with policies
45/// When exported to actual environment, these will always be strings
46#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
47#[serde(untagged)]
48pub enum EnvValue {
49    // Value with policies must come first for serde untagged to try it first
50    WithPolicies(EnvVarWithPolicies),
51    // Simple values (backward compatible)
52    String(String),
53    Int(i64),
54    Bool(bool),
55    Secret(crate::secrets::Secret),
56}
57
58/// Environment configuration with environment-specific overrides
59/// Based on schema/env.cue
60#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
61pub struct Env {
62    /// Base environment variables
63    /// Keys must match pattern: ^[A-Z][A-Z0-9_]*$
64    #[serde(flatten)]
65    pub base: HashMap<String, EnvValue>,
66
67    /// Environment-specific overrides
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub environment: Option<HashMap<String, HashMap<String, EnvValue>>>,
70}
71
72impl Env {
73    /// Get environment variables for a specific environment
74    pub fn for_environment(&self, env_name: &str) -> HashMap<String, EnvValue> {
75        let mut result = self.base.clone();
76
77        if let Some(environments) = &self.environment
78            && let Some(env_overrides) = environments.get(env_name)
79        {
80            result.extend(env_overrides.clone());
81        }
82
83        result
84    }
85}
86
87impl EnvValue {
88    /// Check if a task has access to this environment variable
89    pub fn is_accessible_by_task(&self, task_name: &str) -> bool {
90        match self {
91            // Simple values are always accessible
92            EnvValue::String(_) | EnvValue::Int(_) | EnvValue::Bool(_) | EnvValue::Secret(_) => {
93                true
94            }
95
96            // Check policies for restricted variables
97            EnvValue::WithPolicies(var) => match &var.policies {
98                None => true,                                  // No policies means accessible
99                Some(policies) if policies.is_empty() => true, // Empty policies means accessible
100                Some(policies) => {
101                    // Check if any policy allows this task
102                    policies.iter().any(|policy| {
103                        policy
104                            .allow_tasks
105                            .as_ref()
106                            .is_some_and(|tasks| tasks.iter().any(|t| t == task_name))
107                    })
108                }
109            },
110        }
111    }
112
113    /// Check if an exec command has access to this environment variable
114    pub fn is_accessible_by_exec(&self, command: &str) -> bool {
115        match self {
116            // Simple values are always accessible
117            EnvValue::String(_) | EnvValue::Int(_) | EnvValue::Bool(_) | EnvValue::Secret(_) => {
118                true
119            }
120
121            // Check policies for restricted variables
122            EnvValue::WithPolicies(var) => match &var.policies {
123                None => true,                                  // No policies means accessible
124                Some(policies) if policies.is_empty() => true, // Empty policies means accessible
125                Some(policies) => {
126                    // Check if any policy allows this exec command
127                    policies.iter().any(|policy| {
128                        policy
129                            .allow_exec
130                            .as_ref()
131                            .is_some_and(|execs| execs.iter().any(|e| e == command))
132                    })
133                }
134            },
135        }
136    }
137
138    /// Get the actual string value of the environment variable
139    pub fn to_string_value(&self) -> String {
140        match self {
141            EnvValue::String(s) => s.clone(),
142            EnvValue::Int(i) => i.to_string(),
143            EnvValue::Bool(b) => b.to_string(),
144            EnvValue::Secret(_) => "[SECRET]".to_string(), // Placeholder for secrets
145            EnvValue::WithPolicies(var) => match &var.value {
146                EnvValueSimple::String(s) => s.clone(),
147                EnvValueSimple::Int(i) => i.to_string(),
148                EnvValueSimple::Bool(b) => b.to_string(),
149                EnvValueSimple::Secret(_) => "[SECRET]".to_string(),
150            },
151        }
152    }
153}
154
155/// Runtime environment variables for task execution
156#[derive(Debug, Clone, Serialize, Deserialize, Default)]
157pub struct Environment {
158    /// Map of environment variable names to values
159    #[serde(flatten)]
160    pub vars: HashMap<String, String>,
161}
162
163impl Environment {
164    /// Create a new empty environment
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// Create environment from a map
170    pub fn from_map(vars: HashMap<String, String>) -> Self {
171        Self { vars }
172    }
173
174    /// Get an environment variable value
175    pub fn get(&self, key: &str) -> Option<&str> {
176        self.vars.get(key).map(|s| s.as_str())
177    }
178
179    /// Set an environment variable
180    pub fn set(&mut self, key: String, value: String) {
181        self.vars.insert(key, value);
182    }
183
184    /// Check if an environment variable exists
185    pub fn contains(&self, key: &str) -> bool {
186        self.vars.contains_key(key)
187    }
188
189    /// Get all environment variables as a vector of key=value strings
190    pub fn to_env_vec(&self) -> Vec<String> {
191        self.vars
192            .iter()
193            .map(|(k, v)| format!("{}={}", k, v))
194            .collect()
195    }
196
197    /// Merge with system environment variables
198    /// CUE environment variables take precedence
199    pub fn merge_with_system(&self) -> HashMap<String, String> {
200        let mut merged: HashMap<String, String> = env::vars().collect();
201
202        // Override with CUE environment variables
203        for (key, value) in &self.vars {
204            merged.insert(key.clone(), value.clone());
205        }
206
207        merged
208    }
209
210    /// Convert to a vector of key=value strings including system environment
211    pub fn to_full_env_vec(&self) -> Vec<String> {
212        self.merge_with_system()
213            .iter()
214            .map(|(k, v)| format!("{}={}", k, v))
215            .collect()
216    }
217
218    /// Get the number of environment variables
219    pub fn len(&self) -> usize {
220        self.vars.len()
221    }
222
223    /// Check if the environment is empty
224    pub fn is_empty(&self) -> bool {
225        self.vars.is_empty()
226    }
227
228    /// Iterate over environment variables
229    pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
230        self.vars.iter()
231    }
232
233    /// Build environment for a task, filtering based on policies
234    pub fn build_for_task(
235        task_name: &str,
236        env_vars: &HashMap<String, EnvValue>,
237    ) -> HashMap<String, String> {
238        env_vars
239            .iter()
240            .filter(|(_, value)| value.is_accessible_by_task(task_name))
241            .map(|(key, value)| (key.clone(), value.to_string_value()))
242            .collect()
243    }
244
245    /// Build environment for exec command, filtering based on policies
246    pub fn build_for_exec(
247        command: &str,
248        env_vars: &HashMap<String, EnvValue>,
249    ) -> HashMap<String, String> {
250        env_vars
251            .iter()
252            .filter(|(_, value)| value.is_accessible_by_exec(command))
253            .map(|(key, value)| (key.clone(), value.to_string_value()))
254            .collect()
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_environment_basics() {
264        let mut env = Environment::new();
265        assert!(env.is_empty());
266
267        env.set("FOO".to_string(), "bar".to_string());
268        assert_eq!(env.len(), 1);
269        assert!(env.contains("FOO"));
270        assert_eq!(env.get("FOO"), Some("bar"));
271        assert!(!env.contains("BAR"));
272    }
273
274    #[test]
275    fn test_environment_from_map() {
276        let mut vars = HashMap::new();
277        vars.insert("KEY1".to_string(), "value1".to_string());
278        vars.insert("KEY2".to_string(), "value2".to_string());
279
280        let env = Environment::from_map(vars);
281        assert_eq!(env.len(), 2);
282        assert_eq!(env.get("KEY1"), Some("value1"));
283        assert_eq!(env.get("KEY2"), Some("value2"));
284    }
285
286    #[test]
287    fn test_environment_to_vec() {
288        let mut env = Environment::new();
289        env.set("VAR1".to_string(), "val1".to_string());
290        env.set("VAR2".to_string(), "val2".to_string());
291
292        let vec = env.to_env_vec();
293        assert_eq!(vec.len(), 2);
294        assert!(vec.contains(&"VAR1=val1".to_string()));
295        assert!(vec.contains(&"VAR2=val2".to_string()));
296    }
297
298    #[test]
299    fn test_environment_merge_with_system() {
300        let mut env = Environment::new();
301        env.set("PATH".to_string(), "/custom/path".to_string());
302        env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
303
304        let merged = env.merge_with_system();
305
306        // Custom variables should be present
307        assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
308        assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
309
310        // System variables should still be present (like HOME, USER, etc.)
311        // We can't test specific values but we can check that merging happened
312        assert!(merged.len() >= 2);
313    }
314
315    #[test]
316    fn test_environment_iteration() {
317        let mut env = Environment::new();
318        env.set("A".to_string(), "1".to_string());
319        env.set("B".to_string(), "2".to_string());
320
321        let mut count = 0;
322        for (key, value) in env.iter() {
323            assert!(key == "A" || key == "B");
324            assert!(value == "1" || value == "2");
325            count += 1;
326        }
327        assert_eq!(count, 2);
328    }
329
330    #[test]
331    fn test_env_value_types() {
332        let str_val = EnvValue::String("test".to_string());
333        let int_val = EnvValue::Int(42);
334        let bool_val = EnvValue::Bool(true);
335
336        assert_eq!(str_val, EnvValue::String("test".to_string()));
337        assert_eq!(int_val, EnvValue::Int(42));
338        assert_eq!(bool_val, EnvValue::Bool(true));
339    }
340
341    #[test]
342    fn test_policy_task_access() {
343        // Simple value - always accessible
344        let simple_var = EnvValue::String("simple".to_string());
345        assert!(simple_var.is_accessible_by_task("any_task"));
346
347        // Variable with no policies - accessible
348        let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
349            value: EnvValueSimple::String("value".to_string()),
350            policies: None,
351        });
352        assert!(no_policy_var.is_accessible_by_task("any_task"));
353
354        // Variable with empty policies - accessible
355        let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
356            value: EnvValueSimple::String("value".to_string()),
357            policies: Some(vec![]),
358        });
359        assert!(empty_policy_var.is_accessible_by_task("any_task"));
360
361        // Variable with task restrictions
362        let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
363            value: EnvValueSimple::String("secret".to_string()),
364            policies: Some(vec![Policy {
365                allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
366                allow_exec: None,
367            }]),
368        });
369        assert!(restricted_var.is_accessible_by_task("deploy"));
370        assert!(restricted_var.is_accessible_by_task("release"));
371        assert!(!restricted_var.is_accessible_by_task("test"));
372        assert!(!restricted_var.is_accessible_by_task("build"));
373    }
374
375    #[test]
376    fn test_policy_exec_access() {
377        // Simple value - always accessible
378        let simple_var = EnvValue::String("simple".to_string());
379        assert!(simple_var.is_accessible_by_exec("bash"));
380
381        // Variable with exec restrictions
382        let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
383            value: EnvValueSimple::String("secret".to_string()),
384            policies: Some(vec![Policy {
385                allow_tasks: None,
386                allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
387            }]),
388        });
389        assert!(restricted_var.is_accessible_by_exec("kubectl"));
390        assert!(restricted_var.is_accessible_by_exec("terraform"));
391        assert!(!restricted_var.is_accessible_by_exec("bash"));
392        assert!(!restricted_var.is_accessible_by_exec("sh"));
393    }
394
395    #[test]
396    fn test_multiple_policies() {
397        // Variable with multiple policies - should allow if ANY policy allows
398        let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
399            value: EnvValueSimple::String("value".to_string()),
400            policies: Some(vec![
401                Policy {
402                    allow_tasks: Some(vec!["task1".to_string()]),
403                    allow_exec: None,
404                },
405                Policy {
406                    allow_tasks: Some(vec!["task2".to_string()]),
407                    allow_exec: Some(vec!["kubectl".to_string()]),
408                },
409            ]),
410        });
411
412        // Task access - either policy allows
413        assert!(multi_policy_var.is_accessible_by_task("task1"));
414        assert!(multi_policy_var.is_accessible_by_task("task2"));
415        assert!(!multi_policy_var.is_accessible_by_task("task3"));
416
417        // Exec access - only second policy has exec rules
418        assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
419        assert!(!multi_policy_var.is_accessible_by_exec("bash"));
420    }
421
422    #[test]
423    fn test_to_string_value() {
424        assert_eq!(
425            EnvValue::String("test".to_string()).to_string_value(),
426            "test"
427        );
428        assert_eq!(EnvValue::Int(42).to_string_value(), "42");
429        assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
430        assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
431
432        let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
433            value: EnvValueSimple::String("policy_value".to_string()),
434            policies: Some(vec![]),
435        });
436        assert_eq!(with_policies.to_string_value(), "policy_value");
437    }
438
439    #[test]
440    fn test_build_for_task() {
441        let mut env_vars = HashMap::new();
442
443        // Unrestricted variable
444        env_vars.insert(
445            "PUBLIC".to_string(),
446            EnvValue::String("public_value".to_string()),
447        );
448
449        // Restricted variable
450        env_vars.insert(
451            "SECRET".to_string(),
452            EnvValue::WithPolicies(EnvVarWithPolicies {
453                value: EnvValueSimple::String("secret_value".to_string()),
454                policies: Some(vec![Policy {
455                    allow_tasks: Some(vec!["deploy".to_string()]),
456                    allow_exec: None,
457                }]),
458            }),
459        );
460
461        // Build for deploy task - should get both
462        let deploy_env = Environment::build_for_task("deploy", &env_vars);
463        assert_eq!(deploy_env.len(), 2);
464        assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
465        assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
466
467        // Build for test task - should only get public
468        let test_env = Environment::build_for_task("test", &env_vars);
469        assert_eq!(test_env.len(), 1);
470        assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
471        assert_eq!(test_env.get("SECRET"), None);
472    }
473
474    #[test]
475    fn test_build_for_exec() {
476        let mut env_vars = HashMap::new();
477
478        // Unrestricted variable
479        env_vars.insert(
480            "PUBLIC".to_string(),
481            EnvValue::String("public_value".to_string()),
482        );
483
484        // Restricted variable
485        env_vars.insert(
486            "SECRET".to_string(),
487            EnvValue::WithPolicies(EnvVarWithPolicies {
488                value: EnvValueSimple::String("secret_value".to_string()),
489                policies: Some(vec![Policy {
490                    allow_tasks: None,
491                    allow_exec: Some(vec!["kubectl".to_string()]),
492                }]),
493            }),
494        );
495
496        // Build for kubectl - should get both
497        let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
498        assert_eq!(kubectl_env.len(), 2);
499        assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
500        assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
501
502        // Build for bash - should only get public
503        let bash_env = Environment::build_for_exec("bash", &env_vars);
504        assert_eq!(bash_env.len(), 1);
505        assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
506        assert_eq!(bash_env.get("SECRET"), None);
507    }
508
509    #[test]
510    fn test_env_for_environment() {
511        let mut base = HashMap::new();
512        base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
513        base.insert(
514            "OVERRIDE_ME".to_string(),
515            EnvValue::String("original".to_string()),
516        );
517
518        let mut dev_env = HashMap::new();
519        dev_env.insert(
520            "OVERRIDE_ME".to_string(),
521            EnvValue::String("dev".to_string()),
522        );
523        dev_env.insert(
524            "DEV_VAR".to_string(),
525            EnvValue::String("development".to_string()),
526        );
527
528        let mut environments = HashMap::new();
529        environments.insert("development".to_string(), dev_env);
530
531        let env = Env {
532            base,
533            environment: Some(environments),
534        };
535
536        let dev_vars = env.for_environment("development");
537        assert_eq!(
538            dev_vars.get("BASE_VAR"),
539            Some(&EnvValue::String("base".to_string()))
540        );
541        assert_eq!(
542            dev_vars.get("OVERRIDE_ME"),
543            Some(&EnvValue::String("dev".to_string()))
544        );
545        assert_eq!(
546            dev_vars.get("DEV_VAR"),
547            Some(&EnvValue::String("development".to_string()))
548        );
549    }
550}