1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::env;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct Policy {
13 #[serde(skip_serializing_if = "Option::is_none", rename = "allowTasks")]
15 pub allow_tasks: Option<Vec<String>>,
16
17 #[serde(skip_serializing_if = "Option::is_none", rename = "allowExec")]
19 pub allow_exec: Option<Vec<String>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub struct EnvVarWithPolicies {
25 pub value: EnvValueSimple,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub policies: Option<Vec<Policy>>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35#[serde(untagged)]
36pub enum EnvValueSimple {
37 String(String),
38 Int(i64),
39 Bool(bool),
40 Secret(crate::secrets::Secret),
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46#[serde(untagged)]
47pub enum EnvValue {
48 WithPolicies(EnvVarWithPolicies),
50 String(String),
52 Int(i64),
53 Bool(bool),
54 Secret(crate::secrets::Secret),
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
60pub struct Env {
61 #[serde(flatten)]
64 pub base: HashMap<String, EnvValue>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub environment: Option<HashMap<String, HashMap<String, EnvValue>>>,
69}
70
71impl Env {
72 pub fn for_environment(&self, env_name: &str) -> HashMap<String, EnvValue> {
74 let mut result = self.base.clone();
75
76 if let Some(environments) = &self.environment
77 && let Some(env_overrides) = environments.get(env_name)
78 {
79 result.extend(env_overrides.clone());
80 }
81
82 result
83 }
84}
85
86impl EnvValue {
87 pub fn is_accessible_by_task(&self, task_name: &str) -> bool {
89 match self {
90 EnvValue::String(_) | EnvValue::Int(_) | EnvValue::Bool(_) | EnvValue::Secret(_) => {
92 true
93 }
94
95 EnvValue::WithPolicies(var) => match &var.policies {
97 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
100 policies.iter().any(|policy| {
102 policy
103 .allow_tasks
104 .as_ref()
105 .is_some_and(|tasks| tasks.iter().any(|t| t == task_name))
106 })
107 }
108 },
109 }
110 }
111
112 pub fn is_accessible_by_exec(&self, command: &str) -> bool {
114 match self {
115 EnvValue::String(_) | EnvValue::Int(_) | EnvValue::Bool(_) | EnvValue::Secret(_) => {
117 true
118 }
119
120 EnvValue::WithPolicies(var) => match &var.policies {
122 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
125 policies.iter().any(|policy| {
127 policy
128 .allow_exec
129 .as_ref()
130 .is_some_and(|execs| execs.iter().any(|e| e == command))
131 })
132 }
133 },
134 }
135 }
136
137 pub fn to_string_value(&self) -> String {
139 match self {
140 EnvValue::String(s) => s.clone(),
141 EnvValue::Int(i) => i.to_string(),
142 EnvValue::Bool(b) => b.to_string(),
143 EnvValue::Secret(_) => "[SECRET]".to_string(), EnvValue::WithPolicies(var) => match &var.value {
145 EnvValueSimple::String(s) => s.clone(),
146 EnvValueSimple::Int(i) => i.to_string(),
147 EnvValueSimple::Bool(b) => b.to_string(),
148 EnvValueSimple::Secret(_) => "[SECRET]".to_string(),
149 },
150 }
151 }
152
153 pub async fn resolve(&self) -> crate::Result<String> {
155 match self {
156 EnvValue::String(s) => Ok(s.clone()),
157 EnvValue::Int(i) => Ok(i.to_string()),
158 EnvValue::Bool(b) => Ok(b.to_string()),
159 EnvValue::Secret(s) => s.resolve().await,
160 EnvValue::WithPolicies(var) => match &var.value {
161 EnvValueSimple::String(s) => Ok(s.clone()),
162 EnvValueSimple::Int(i) => Ok(i.to_string()),
163 EnvValueSimple::Bool(b) => Ok(b.to_string()),
164 EnvValueSimple::Secret(s) => s.resolve().await,
165 },
166 }
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172pub struct Environment {
173 #[serde(flatten)]
175 pub vars: HashMap<String, String>,
176}
177
178impl Environment {
179 pub fn new() -> Self {
181 Self::default()
182 }
183
184 pub fn from_map(vars: HashMap<String, String>) -> Self {
186 Self { vars }
187 }
188
189 pub fn get(&self, key: &str) -> Option<&str> {
191 self.vars.get(key).map(|s| s.as_str())
192 }
193
194 pub fn set(&mut self, key: String, value: String) {
196 self.vars.insert(key, value);
197 }
198
199 pub fn contains(&self, key: &str) -> bool {
201 self.vars.contains_key(key)
202 }
203
204 pub fn to_env_vec(&self) -> Vec<String> {
206 self.vars
207 .iter()
208 .map(|(k, v)| format!("{}={}", k, v))
209 .collect()
210 }
211
212 pub fn merge_with_system(&self) -> HashMap<String, String> {
215 let mut merged: HashMap<String, String> = env::vars().collect();
216
217 for (key, value) in &self.vars {
219 merged.insert(key.clone(), value.clone());
220 }
221
222 merged
223 }
224
225 pub fn to_full_env_vec(&self) -> Vec<String> {
227 self.merge_with_system()
228 .iter()
229 .map(|(k, v)| format!("{}={}", k, v))
230 .collect()
231 }
232
233 pub fn len(&self) -> usize {
235 self.vars.len()
236 }
237
238 pub fn is_empty(&self) -> bool {
240 self.vars.is_empty()
241 }
242
243 pub fn resolve_command(&self, command: &str) -> String {
251 if command.starts_with('/') {
253 tracing::debug!(command = %command, "Command is already absolute path");
254 return command.to_string();
255 }
256
257 let path_value = self
259 .vars
260 .get("PATH")
261 .cloned()
262 .or_else(|| env::var("PATH").ok())
263 .unwrap_or_default();
264
265 tracing::debug!(
266 command = %command,
267 env_has_path = self.vars.contains_key("PATH"),
268 path_len = path_value.len(),
269 "Resolving command in PATH"
270 );
271
272 for dir in path_value.split(':') {
274 if dir.is_empty() {
275 continue;
276 }
277 let candidate = std::path::Path::new(dir).join(command);
278 if candidate.is_file() {
279 #[cfg(unix)]
281 {
282 use std::os::unix::fs::PermissionsExt;
283 if let Ok(metadata) = std::fs::metadata(&candidate) {
284 let permissions = metadata.permissions();
285 if permissions.mode() & 0o111 != 0 {
286 tracing::debug!(
287 command = %command,
288 resolved = %candidate.display(),
289 "Command resolved to path"
290 );
291 return candidate.to_string_lossy().to_string();
292 }
293 }
294 }
295 #[cfg(not(unix))]
296 {
297 tracing::debug!(
298 command = %command,
299 resolved = %candidate.display(),
300 "Command resolved to path"
301 );
302 return candidate.to_string_lossy().to_string();
303 }
304 }
305 }
306
307 tracing::warn!(
309 command = %command,
310 env_path_set = self.vars.contains_key("PATH"),
311 "Command not found in PATH, returning original"
312 );
313 command.to_string()
314 }
315
316 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
318 self.vars.iter()
319 }
320
321 pub fn build_for_task(
323 task_name: &str,
324 env_vars: &HashMap<String, EnvValue>,
325 ) -> HashMap<String, String> {
326 env_vars
327 .iter()
328 .filter(|(_, value)| value.is_accessible_by_task(task_name))
329 .map(|(key, value)| (key.clone(), value.to_string_value()))
330 .collect()
331 }
332
333 pub async fn resolve_for_task(
335 task_name: &str,
336 env_vars: &HashMap<String, EnvValue>,
337 ) -> crate::Result<HashMap<String, String>> {
338 let mut resolved = HashMap::new();
339 for (key, value) in env_vars {
340 if value.is_accessible_by_task(task_name) {
341 resolved.insert(key.clone(), value.resolve().await?);
342 }
343 }
344 Ok(resolved)
345 }
346
347 pub fn build_for_exec(
349 command: &str,
350 env_vars: &HashMap<String, EnvValue>,
351 ) -> HashMap<String, String> {
352 env_vars
353 .iter()
354 .filter(|(_, value)| value.is_accessible_by_exec(command))
355 .map(|(key, value)| (key.clone(), value.to_string_value()))
356 .collect()
357 }
358
359 pub async fn resolve_for_exec(
361 command: &str,
362 env_vars: &HashMap<String, EnvValue>,
363 ) -> crate::Result<HashMap<String, String>> {
364 let mut resolved = HashMap::new();
365 for (key, value) in env_vars {
366 if value.is_accessible_by_exec(command) {
367 resolved.insert(key.clone(), value.resolve().await?);
368 }
369 }
370 Ok(resolved)
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_environment_basics() {
380 let mut env = Environment::new();
381 assert!(env.is_empty());
382
383 env.set("FOO".to_string(), "bar".to_string());
384 assert_eq!(env.len(), 1);
385 assert!(env.contains("FOO"));
386 assert_eq!(env.get("FOO"), Some("bar"));
387 assert!(!env.contains("BAR"));
388 }
389
390 #[test]
391 fn test_environment_from_map() {
392 let mut vars = HashMap::new();
393 vars.insert("KEY1".to_string(), "value1".to_string());
394 vars.insert("KEY2".to_string(), "value2".to_string());
395
396 let env = Environment::from_map(vars);
397 assert_eq!(env.len(), 2);
398 assert_eq!(env.get("KEY1"), Some("value1"));
399 assert_eq!(env.get("KEY2"), Some("value2"));
400 }
401
402 #[test]
403 fn test_environment_to_vec() {
404 let mut env = Environment::new();
405 env.set("VAR1".to_string(), "val1".to_string());
406 env.set("VAR2".to_string(), "val2".to_string());
407
408 let vec = env.to_env_vec();
409 assert_eq!(vec.len(), 2);
410 assert!(vec.contains(&"VAR1=val1".to_string()));
411 assert!(vec.contains(&"VAR2=val2".to_string()));
412 }
413
414 #[test]
415 fn test_environment_merge_with_system() {
416 let mut env = Environment::new();
417 env.set("PATH".to_string(), "/custom/path".to_string());
418 env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
419
420 let merged = env.merge_with_system();
421
422 assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
424 assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
425
426 assert!(merged.len() >= 2);
429 }
430
431 #[test]
432 fn test_environment_iteration() {
433 let mut env = Environment::new();
434 env.set("A".to_string(), "1".to_string());
435 env.set("B".to_string(), "2".to_string());
436
437 let mut count = 0;
438 for (key, value) in env.iter() {
439 assert!(key == "A" || key == "B");
440 assert!(value == "1" || value == "2");
441 count += 1;
442 }
443 assert_eq!(count, 2);
444 }
445
446 #[test]
447 fn test_env_value_types() {
448 let str_val = EnvValue::String("test".to_string());
449 let int_val = EnvValue::Int(42);
450 let bool_val = EnvValue::Bool(true);
451
452 assert_eq!(str_val, EnvValue::String("test".to_string()));
453 assert_eq!(int_val, EnvValue::Int(42));
454 assert_eq!(bool_val, EnvValue::Bool(true));
455 }
456
457 #[test]
458 fn test_policy_task_access() {
459 let simple_var = EnvValue::String("simple".to_string());
461 assert!(simple_var.is_accessible_by_task("any_task"));
462
463 let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
465 value: EnvValueSimple::String("value".to_string()),
466 policies: None,
467 });
468 assert!(no_policy_var.is_accessible_by_task("any_task"));
469
470 let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
472 value: EnvValueSimple::String("value".to_string()),
473 policies: Some(vec![]),
474 });
475 assert!(empty_policy_var.is_accessible_by_task("any_task"));
476
477 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
479 value: EnvValueSimple::String("secret".to_string()),
480 policies: Some(vec![Policy {
481 allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
482 allow_exec: None,
483 }]),
484 });
485 assert!(restricted_var.is_accessible_by_task("deploy"));
486 assert!(restricted_var.is_accessible_by_task("release"));
487 assert!(!restricted_var.is_accessible_by_task("test"));
488 assert!(!restricted_var.is_accessible_by_task("build"));
489 }
490
491 #[test]
492 fn test_policy_exec_access() {
493 let simple_var = EnvValue::String("simple".to_string());
495 assert!(simple_var.is_accessible_by_exec("bash"));
496
497 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
499 value: EnvValueSimple::String("secret".to_string()),
500 policies: Some(vec![Policy {
501 allow_tasks: None,
502 allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
503 }]),
504 });
505 assert!(restricted_var.is_accessible_by_exec("kubectl"));
506 assert!(restricted_var.is_accessible_by_exec("terraform"));
507 assert!(!restricted_var.is_accessible_by_exec("bash"));
508 assert!(!restricted_var.is_accessible_by_exec("sh"));
509 }
510
511 #[test]
512 fn test_multiple_policies() {
513 let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
515 value: EnvValueSimple::String("value".to_string()),
516 policies: Some(vec![
517 Policy {
518 allow_tasks: Some(vec!["task1".to_string()]),
519 allow_exec: None,
520 },
521 Policy {
522 allow_tasks: Some(vec!["task2".to_string()]),
523 allow_exec: Some(vec!["kubectl".to_string()]),
524 },
525 ]),
526 });
527
528 assert!(multi_policy_var.is_accessible_by_task("task1"));
530 assert!(multi_policy_var.is_accessible_by_task("task2"));
531 assert!(!multi_policy_var.is_accessible_by_task("task3"));
532
533 assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
535 assert!(!multi_policy_var.is_accessible_by_exec("bash"));
536 }
537
538 #[test]
539 fn test_to_string_value() {
540 assert_eq!(
541 EnvValue::String("test".to_string()).to_string_value(),
542 "test"
543 );
544 assert_eq!(EnvValue::Int(42).to_string_value(), "42");
545 assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
546 assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
547
548 let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
549 value: EnvValueSimple::String("policy_value".to_string()),
550 policies: Some(vec![]),
551 });
552 assert_eq!(with_policies.to_string_value(), "policy_value");
553 }
554
555 #[test]
556 fn test_build_for_task() {
557 let mut env_vars = HashMap::new();
558
559 env_vars.insert(
561 "PUBLIC".to_string(),
562 EnvValue::String("public_value".to_string()),
563 );
564
565 env_vars.insert(
567 "SECRET".to_string(),
568 EnvValue::WithPolicies(EnvVarWithPolicies {
569 value: EnvValueSimple::String("secret_value".to_string()),
570 policies: Some(vec![Policy {
571 allow_tasks: Some(vec!["deploy".to_string()]),
572 allow_exec: None,
573 }]),
574 }),
575 );
576
577 let deploy_env = Environment::build_for_task("deploy", &env_vars);
579 assert_eq!(deploy_env.len(), 2);
580 assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
581 assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
582
583 let test_env = Environment::build_for_task("test", &env_vars);
585 assert_eq!(test_env.len(), 1);
586 assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
587 assert_eq!(test_env.get("SECRET"), None);
588 }
589
590 #[test]
591 fn test_build_for_exec() {
592 let mut env_vars = HashMap::new();
593
594 env_vars.insert(
596 "PUBLIC".to_string(),
597 EnvValue::String("public_value".to_string()),
598 );
599
600 env_vars.insert(
602 "SECRET".to_string(),
603 EnvValue::WithPolicies(EnvVarWithPolicies {
604 value: EnvValueSimple::String("secret_value".to_string()),
605 policies: Some(vec![Policy {
606 allow_tasks: None,
607 allow_exec: Some(vec!["kubectl".to_string()]),
608 }]),
609 }),
610 );
611
612 let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
614 assert_eq!(kubectl_env.len(), 2);
615 assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
616 assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
617
618 let bash_env = Environment::build_for_exec("bash", &env_vars);
620 assert_eq!(bash_env.len(), 1);
621 assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
622 assert_eq!(bash_env.get("SECRET"), None);
623 }
624
625 #[test]
626 fn test_env_for_environment() {
627 let mut base = HashMap::new();
628 base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
629 base.insert(
630 "OVERRIDE_ME".to_string(),
631 EnvValue::String("original".to_string()),
632 );
633
634 let mut dev_env = HashMap::new();
635 dev_env.insert(
636 "OVERRIDE_ME".to_string(),
637 EnvValue::String("dev".to_string()),
638 );
639 dev_env.insert(
640 "DEV_VAR".to_string(),
641 EnvValue::String("development".to_string()),
642 );
643
644 let mut environments = HashMap::new();
645 environments.insert("development".to_string(), dev_env);
646
647 let env = Env {
648 base,
649 environment: Some(environments),
650 };
651
652 let dev_vars = env.for_environment("development");
653 assert_eq!(
654 dev_vars.get("BASE_VAR"),
655 Some(&EnvValue::String("base".to_string()))
656 );
657 assert_eq!(
658 dev_vars.get("OVERRIDE_ME"),
659 Some(&EnvValue::String("dev".to_string()))
660 );
661 assert_eq!(
662 dev_vars.get("DEV_VAR"),
663 Some(&EnvValue::String("development".to_string()))
664 );
665 }
666}