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 #[must_use]
155 pub fn is_secret(&self) -> bool {
156 match self {
157 EnvValue::Secret(_) => true,
158 EnvValue::WithPolicies(var) => matches!(var.value, EnvValueSimple::Secret(_)),
159 _ => false,
160 }
161 }
162
163 pub async fn resolve(&self) -> crate::Result<String> {
168 match self {
169 EnvValue::String(s) => Ok(s.clone()),
170 EnvValue::Int(i) => Ok(i.to_string()),
171 EnvValue::Bool(b) => Ok(b.to_string()),
172 EnvValue::Secret(s) => s.resolve().await,
173 EnvValue::WithPolicies(var) => match &var.value {
174 EnvValueSimple::String(s) => Ok(s.clone()),
175 EnvValueSimple::Int(i) => Ok(i.to_string()),
176 EnvValueSimple::Bool(b) => Ok(b.to_string()),
177 EnvValueSimple::Secret(s) => s.resolve().await,
178 },
179 }
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, Default)]
185pub struct Environment {
186 #[serde(flatten)]
188 pub vars: HashMap<String, String>,
189}
190
191impl Environment {
192 pub fn new() -> Self {
194 Self::default()
195 }
196
197 pub fn from_map(vars: HashMap<String, String>) -> Self {
199 Self { vars }
200 }
201
202 pub fn get(&self, key: &str) -> Option<&str> {
204 self.vars.get(key).map(|s| s.as_str())
205 }
206
207 pub fn set(&mut self, key: String, value: String) {
209 self.vars.insert(key, value);
210 }
211
212 pub fn contains(&self, key: &str) -> bool {
214 self.vars.contains_key(key)
215 }
216
217 pub fn to_env_vec(&self) -> Vec<String> {
219 self.vars
220 .iter()
221 .map(|(k, v)| format!("{}={}", k, v))
222 .collect()
223 }
224
225 pub fn merge_with_system(&self) -> HashMap<String, String> {
228 let mut merged: HashMap<String, String> = env::vars().collect();
229
230 for (key, value) in &self.vars {
232 merged.insert(key.clone(), value.clone());
233 }
234
235 merged
236 }
237
238 pub fn to_full_env_vec(&self) -> Vec<String> {
240 self.merge_with_system()
241 .iter()
242 .map(|(k, v)| format!("{}={}", k, v))
243 .collect()
244 }
245
246 pub fn len(&self) -> usize {
248 self.vars.len()
249 }
250
251 pub fn is_empty(&self) -> bool {
253 self.vars.is_empty()
254 }
255
256 pub fn resolve_command(&self, command: &str) -> String {
264 if command.starts_with('/') {
266 tracing::debug!(command = %command, "Command is already absolute path");
267 return command.to_string();
268 }
269
270 let path_value = self
272 .vars
273 .get("PATH")
274 .cloned()
275 .or_else(|| env::var("PATH").ok())
276 .unwrap_or_default();
277
278 tracing::debug!(
279 command = %command,
280 env_has_path = self.vars.contains_key("PATH"),
281 path_len = path_value.len(),
282 "Resolving command in PATH"
283 );
284
285 for dir in path_value.split(':') {
287 if dir.is_empty() {
288 continue;
289 }
290 let candidate = std::path::Path::new(dir).join(command);
291 if candidate.is_file() {
292 #[cfg(unix)]
294 {
295 use std::os::unix::fs::PermissionsExt;
296 if let Ok(metadata) = std::fs::metadata(&candidate) {
297 let permissions = metadata.permissions();
298 if permissions.mode() & 0o111 != 0 {
299 tracing::debug!(
300 command = %command,
301 resolved = %candidate.display(),
302 "Command resolved to path"
303 );
304 return candidate.to_string_lossy().to_string();
305 }
306 }
307 }
308 #[cfg(not(unix))]
309 {
310 tracing::debug!(
311 command = %command,
312 resolved = %candidate.display(),
313 "Command resolved to path"
314 );
315 return candidate.to_string_lossy().to_string();
316 }
317 }
318 }
319
320 tracing::warn!(
322 command = %command,
323 env_path_set = self.vars.contains_key("PATH"),
324 "Command not found in PATH, returning original"
325 );
326 command.to_string()
327 }
328
329 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
331 self.vars.iter()
332 }
333
334 pub fn build_for_task(
336 task_name: &str,
337 env_vars: &HashMap<String, EnvValue>,
338 ) -> HashMap<String, String> {
339 env_vars
340 .iter()
341 .filter(|(_, value)| value.is_accessible_by_task(task_name))
342 .map(|(key, value)| (key.clone(), value.to_string_value()))
343 .collect()
344 }
345
346 pub async fn resolve_for_task(
351 task_name: &str,
352 env_vars: &HashMap<String, EnvValue>,
353 ) -> crate::Result<HashMap<String, String>> {
354 let (resolved, _secrets) = Self::resolve_for_task_with_secrets(task_name, env_vars).await?;
355 Ok(resolved)
356 }
357
358 pub async fn resolve_for_task_with_secrets(
363 task_name: &str,
364 env_vars: &HashMap<String, EnvValue>,
365 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
366 let mut resolved = HashMap::new();
367 let mut secrets = Vec::new();
368
369 for (key, value) in env_vars {
370 if value.is_accessible_by_task(task_name) {
371 let resolved_value = value.resolve().await?;
372 if value.is_secret() {
373 secrets.push(resolved_value.clone());
374 }
375 resolved.insert(key.clone(), resolved_value);
376 }
377 }
378 Ok((resolved, secrets))
379 }
380
381 pub fn build_for_exec(
383 command: &str,
384 env_vars: &HashMap<String, EnvValue>,
385 ) -> HashMap<String, String> {
386 env_vars
387 .iter()
388 .filter(|(_, value)| value.is_accessible_by_exec(command))
389 .map(|(key, value)| (key.clone(), value.to_string_value()))
390 .collect()
391 }
392
393 pub async fn resolve_for_exec(
398 command: &str,
399 env_vars: &HashMap<String, EnvValue>,
400 ) -> crate::Result<HashMap<String, String>> {
401 let (resolved, _secrets) = Self::resolve_for_exec_with_secrets(command, env_vars).await?;
402 Ok(resolved)
403 }
404
405 pub async fn resolve_for_exec_with_secrets(
410 command: &str,
411 env_vars: &HashMap<String, EnvValue>,
412 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
413 let mut resolved = HashMap::new();
414 let mut secrets = Vec::new();
415
416 for (key, value) in env_vars {
417 if value.is_accessible_by_exec(command) {
418 let resolved_value = value.resolve().await?;
419 if value.is_secret() {
420 secrets.push(resolved_value.clone());
421 }
422 resolved.insert(key.clone(), resolved_value);
423 }
424 }
425 Ok((resolved, secrets))
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_environment_basics() {
435 let mut env = Environment::new();
436 assert!(env.is_empty());
437
438 env.set("FOO".to_string(), "bar".to_string());
439 assert_eq!(env.len(), 1);
440 assert!(env.contains("FOO"));
441 assert_eq!(env.get("FOO"), Some("bar"));
442 assert!(!env.contains("BAR"));
443 }
444
445 #[test]
446 fn test_environment_from_map() {
447 let mut vars = HashMap::new();
448 vars.insert("KEY1".to_string(), "value1".to_string());
449 vars.insert("KEY2".to_string(), "value2".to_string());
450
451 let env = Environment::from_map(vars);
452 assert_eq!(env.len(), 2);
453 assert_eq!(env.get("KEY1"), Some("value1"));
454 assert_eq!(env.get("KEY2"), Some("value2"));
455 }
456
457 #[test]
458 fn test_environment_to_vec() {
459 let mut env = Environment::new();
460 env.set("VAR1".to_string(), "val1".to_string());
461 env.set("VAR2".to_string(), "val2".to_string());
462
463 let vec = env.to_env_vec();
464 assert_eq!(vec.len(), 2);
465 assert!(vec.contains(&"VAR1=val1".to_string()));
466 assert!(vec.contains(&"VAR2=val2".to_string()));
467 }
468
469 #[test]
470 fn test_environment_merge_with_system() {
471 let mut env = Environment::new();
472 env.set("PATH".to_string(), "/custom/path".to_string());
473 env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
474
475 let merged = env.merge_with_system();
476
477 assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
479 assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
480
481 assert!(merged.len() >= 2);
484 }
485
486 #[test]
487 fn test_environment_iteration() {
488 let mut env = Environment::new();
489 env.set("A".to_string(), "1".to_string());
490 env.set("B".to_string(), "2".to_string());
491
492 let mut count = 0;
493 for (key, value) in env.iter() {
494 assert!(key == "A" || key == "B");
495 assert!(value == "1" || value == "2");
496 count += 1;
497 }
498 assert_eq!(count, 2);
499 }
500
501 #[test]
502 fn test_env_value_types() {
503 let str_val = EnvValue::String("test".to_string());
504 let int_val = EnvValue::Int(42);
505 let bool_val = EnvValue::Bool(true);
506
507 assert_eq!(str_val, EnvValue::String("test".to_string()));
508 assert_eq!(int_val, EnvValue::Int(42));
509 assert_eq!(bool_val, EnvValue::Bool(true));
510 }
511
512 #[test]
513 fn test_policy_task_access() {
514 let simple_var = EnvValue::String("simple".to_string());
516 assert!(simple_var.is_accessible_by_task("any_task"));
517
518 let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
520 value: EnvValueSimple::String("value".to_string()),
521 policies: None,
522 });
523 assert!(no_policy_var.is_accessible_by_task("any_task"));
524
525 let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
527 value: EnvValueSimple::String("value".to_string()),
528 policies: Some(vec![]),
529 });
530 assert!(empty_policy_var.is_accessible_by_task("any_task"));
531
532 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
534 value: EnvValueSimple::String("secret".to_string()),
535 policies: Some(vec![Policy {
536 allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
537 allow_exec: None,
538 }]),
539 });
540 assert!(restricted_var.is_accessible_by_task("deploy"));
541 assert!(restricted_var.is_accessible_by_task("release"));
542 assert!(!restricted_var.is_accessible_by_task("test"));
543 assert!(!restricted_var.is_accessible_by_task("build"));
544 }
545
546 #[test]
547 fn test_policy_exec_access() {
548 let simple_var = EnvValue::String("simple".to_string());
550 assert!(simple_var.is_accessible_by_exec("bash"));
551
552 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
554 value: EnvValueSimple::String("secret".to_string()),
555 policies: Some(vec![Policy {
556 allow_tasks: None,
557 allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
558 }]),
559 });
560 assert!(restricted_var.is_accessible_by_exec("kubectl"));
561 assert!(restricted_var.is_accessible_by_exec("terraform"));
562 assert!(!restricted_var.is_accessible_by_exec("bash"));
563 assert!(!restricted_var.is_accessible_by_exec("sh"));
564 }
565
566 #[test]
567 fn test_multiple_policies() {
568 let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
570 value: EnvValueSimple::String("value".to_string()),
571 policies: Some(vec![
572 Policy {
573 allow_tasks: Some(vec!["task1".to_string()]),
574 allow_exec: None,
575 },
576 Policy {
577 allow_tasks: Some(vec!["task2".to_string()]),
578 allow_exec: Some(vec!["kubectl".to_string()]),
579 },
580 ]),
581 });
582
583 assert!(multi_policy_var.is_accessible_by_task("task1"));
585 assert!(multi_policy_var.is_accessible_by_task("task2"));
586 assert!(!multi_policy_var.is_accessible_by_task("task3"));
587
588 assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
590 assert!(!multi_policy_var.is_accessible_by_exec("bash"));
591 }
592
593 #[test]
594 fn test_to_string_value() {
595 assert_eq!(
596 EnvValue::String("test".to_string()).to_string_value(),
597 "test"
598 );
599 assert_eq!(EnvValue::Int(42).to_string_value(), "42");
600 assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
601 assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
602
603 let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
604 value: EnvValueSimple::String("policy_value".to_string()),
605 policies: Some(vec![]),
606 });
607 assert_eq!(with_policies.to_string_value(), "policy_value");
608 }
609
610 #[test]
611 fn test_build_for_task() {
612 let mut env_vars = HashMap::new();
613
614 env_vars.insert(
616 "PUBLIC".to_string(),
617 EnvValue::String("public_value".to_string()),
618 );
619
620 env_vars.insert(
622 "SECRET".to_string(),
623 EnvValue::WithPolicies(EnvVarWithPolicies {
624 value: EnvValueSimple::String("secret_value".to_string()),
625 policies: Some(vec![Policy {
626 allow_tasks: Some(vec!["deploy".to_string()]),
627 allow_exec: None,
628 }]),
629 }),
630 );
631
632 let deploy_env = Environment::build_for_task("deploy", &env_vars);
634 assert_eq!(deploy_env.len(), 2);
635 assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
636 assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
637
638 let test_env = Environment::build_for_task("test", &env_vars);
640 assert_eq!(test_env.len(), 1);
641 assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
642 assert_eq!(test_env.get("SECRET"), None);
643 }
644
645 #[test]
646 fn test_build_for_exec() {
647 let mut env_vars = HashMap::new();
648
649 env_vars.insert(
651 "PUBLIC".to_string(),
652 EnvValue::String("public_value".to_string()),
653 );
654
655 env_vars.insert(
657 "SECRET".to_string(),
658 EnvValue::WithPolicies(EnvVarWithPolicies {
659 value: EnvValueSimple::String("secret_value".to_string()),
660 policies: Some(vec![Policy {
661 allow_tasks: None,
662 allow_exec: Some(vec!["kubectl".to_string()]),
663 }]),
664 }),
665 );
666
667 let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
669 assert_eq!(kubectl_env.len(), 2);
670 assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
671 assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
672
673 let bash_env = Environment::build_for_exec("bash", &env_vars);
675 assert_eq!(bash_env.len(), 1);
676 assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
677 assert_eq!(bash_env.get("SECRET"), None);
678 }
679
680 #[test]
681 fn test_env_for_environment() {
682 let mut base = HashMap::new();
683 base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
684 base.insert(
685 "OVERRIDE_ME".to_string(),
686 EnvValue::String("original".to_string()),
687 );
688
689 let mut dev_env = HashMap::new();
690 dev_env.insert(
691 "OVERRIDE_ME".to_string(),
692 EnvValue::String("dev".to_string()),
693 );
694 dev_env.insert(
695 "DEV_VAR".to_string(),
696 EnvValue::String("development".to_string()),
697 );
698
699 let mut environments = HashMap::new();
700 environments.insert("development".to_string(), dev_env);
701
702 let env = Env {
703 base,
704 environment: Some(environments),
705 };
706
707 let dev_vars = env.for_environment("development");
708 assert_eq!(
709 dev_vars.get("BASE_VAR"),
710 Some(&EnvValue::String("base".to_string()))
711 );
712 assert_eq!(
713 dev_vars.get("OVERRIDE_ME"),
714 Some(&EnvValue::String("dev".to_string()))
715 );
716 assert_eq!(
717 dev_vars.get("DEV_VAR"),
718 Some(&EnvValue::String("development".to_string()))
719 );
720 }
721
722 #[tokio::test]
723 async fn test_resolve_plain_string() {
724 let env_val = EnvValue::String("plain_value".to_string());
725 let resolved = env_val.resolve().await.unwrap();
726 assert_eq!(resolved, "plain_value");
727 }
728
729 #[tokio::test]
730 async fn test_resolve_int() {
731 let env_val = EnvValue::Int(42);
732 let resolved = env_val.resolve().await.unwrap();
733 assert_eq!(resolved, "42");
734 }
735
736 #[tokio::test]
737 async fn test_resolve_bool() {
738 let env_val = EnvValue::Bool(true);
739 let resolved = env_val.resolve().await.unwrap();
740 assert_eq!(resolved, "true");
741 }
742
743 #[tokio::test]
744 async fn test_resolve_with_policies_plain_string() {
745 let env_val = EnvValue::WithPolicies(EnvVarWithPolicies {
746 value: EnvValueSimple::String("policy_value".to_string()),
747 policies: None,
748 });
749 let resolved = env_val.resolve().await.unwrap();
750 assert_eq!(resolved, "policy_value");
751 }
752}