1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::env;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(untagged)]
14pub enum EnvPart {
15 Secret(crate::secrets::Secret),
17 Literal(String),
19}
20
21impl EnvPart {
22 #[must_use]
24 pub fn is_secret(&self) -> bool {
25 matches!(self, EnvPart::Secret(_))
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub struct Policy {
32 #[serde(skip_serializing_if = "Option::is_none", rename = "allowTasks")]
34 pub allow_tasks: Option<Vec<String>>,
35
36 #[serde(skip_serializing_if = "Option::is_none", rename = "allowExec")]
38 pub allow_exec: Option<Vec<String>>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct EnvVarWithPolicies {
44 pub value: EnvValueSimple,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub policies: Option<Vec<Policy>>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(untagged)]
55pub enum EnvValueSimple {
56 Secret(crate::secrets::Secret),
58 Interpolated(Vec<EnvPart>),
60 String(String),
62 Int(i64),
64 Bool(bool),
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72#[serde(untagged)]
73pub enum EnvValue {
74 WithPolicies(EnvVarWithPolicies),
77 Secret(crate::secrets::Secret),
79 Interpolated(Vec<EnvPart>),
81 String(String),
83 Int(i64),
84 Bool(bool),
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
90pub struct Env {
91 #[serde(flatten)]
94 pub base: HashMap<String, EnvValue>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub environment: Option<HashMap<String, HashMap<String, EnvValue>>>,
99}
100
101impl Env {
102 pub fn for_environment(&self, env_name: &str) -> HashMap<String, EnvValue> {
104 let mut result = self.base.clone();
105
106 if let Some(environments) = &self.environment
107 && let Some(env_overrides) = environments.get(env_name)
108 {
109 result.extend(env_overrides.clone());
110 }
111
112 result
113 }
114}
115
116impl EnvValue {
117 pub fn is_accessible_by_task(&self, task_name: &str) -> bool {
119 match self {
120 EnvValue::String(_)
122 | EnvValue::Int(_)
123 | EnvValue::Bool(_)
124 | EnvValue::Secret(_)
125 | EnvValue::Interpolated(_) => true,
126
127 EnvValue::WithPolicies(var) => match &var.policies {
129 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
132 policies.iter().any(|policy| {
134 policy
135 .allow_tasks
136 .as_ref()
137 .is_some_and(|tasks| tasks.iter().any(|t| t == task_name))
138 })
139 }
140 },
141 }
142 }
143
144 pub fn is_accessible_by_exec(&self, command: &str) -> bool {
146 match self {
147 EnvValue::String(_)
149 | EnvValue::Int(_)
150 | EnvValue::Bool(_)
151 | EnvValue::Secret(_)
152 | EnvValue::Interpolated(_) => true,
153
154 EnvValue::WithPolicies(var) => match &var.policies {
156 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
159 policies.iter().any(|policy| {
161 policy
162 .allow_exec
163 .as_ref()
164 .is_some_and(|execs| execs.iter().any(|e| e == command))
165 })
166 }
167 },
168 }
169 }
170
171 pub fn to_string_value(&self) -> String {
174 match self {
175 EnvValue::String(s) => s.clone(),
176 EnvValue::Int(i) => i.to_string(),
177 EnvValue::Bool(b) => b.to_string(),
178 EnvValue::Secret(_) => cuenv_events::REDACTED_PLACEHOLDER.to_string(),
179 EnvValue::Interpolated(parts) => Self::parts_to_string_value(parts),
180 EnvValue::WithPolicies(var) => match &var.value {
181 EnvValueSimple::String(s) => s.clone(),
182 EnvValueSimple::Int(i) => i.to_string(),
183 EnvValueSimple::Bool(b) => b.to_string(),
184 EnvValueSimple::Secret(_) => cuenv_events::REDACTED_PLACEHOLDER.to_string(),
185 EnvValueSimple::Interpolated(parts) => Self::parts_to_string_value(parts),
186 },
187 }
188 }
189
190 fn parts_to_string_value(parts: &[EnvPart]) -> String {
192 parts
193 .iter()
194 .map(|p| match p {
195 EnvPart::Literal(s) => s.clone(),
196 EnvPart::Secret(_) => cuenv_events::REDACTED_PLACEHOLDER.to_string(),
197 })
198 .collect()
199 }
200
201 #[must_use]
203 pub fn is_secret(&self) -> bool {
204 match self {
205 EnvValue::Secret(_) => true,
206 EnvValue::Interpolated(parts) => parts.iter().any(EnvPart::is_secret),
207 EnvValue::WithPolicies(var) => match &var.value {
208 EnvValueSimple::Secret(_) => true,
209 EnvValueSimple::Interpolated(parts) => parts.iter().any(EnvPart::is_secret),
210 _ => false,
211 },
212 _ => false,
213 }
214 }
215
216 pub async fn resolve(&self) -> crate::Result<String> {
221 let (resolved, _) = self.resolve_with_secrets().await?;
222 Ok(resolved)
223 }
224
225 pub async fn resolve_with_secrets(&self) -> crate::Result<(String, Vec<String>)> {
231 match self {
232 EnvValue::String(s) => Ok((s.clone(), vec![])),
233 EnvValue::Int(i) => Ok((i.to_string(), vec![])),
234 EnvValue::Bool(b) => Ok((b.to_string(), vec![])),
235 EnvValue::Secret(s) => {
236 let resolved = s.resolve().await?;
237 Ok((resolved.clone(), vec![resolved]))
238 }
239 EnvValue::Interpolated(parts) => Self::resolve_parts_with_secrets(parts).await,
240 EnvValue::WithPolicies(var) => Self::resolve_simple_with_secrets(&var.value).await,
241 }
242 }
243
244 async fn resolve_parts_with_secrets(parts: &[EnvPart]) -> crate::Result<(String, Vec<String>)> {
246 let mut result = String::new();
247 let mut secrets = Vec::new();
248
249 for part in parts {
250 match part {
251 EnvPart::Literal(s) => result.push_str(s),
252 EnvPart::Secret(s) => {
253 let resolved = s.resolve().await?;
254 result.push_str(&resolved);
255 secrets.push(resolved);
256 }
257 }
258 }
259
260 Ok((result, secrets))
261 }
262
263 async fn resolve_simple_with_secrets(
265 value: &EnvValueSimple,
266 ) -> crate::Result<(String, Vec<String>)> {
267 match value {
268 EnvValueSimple::String(s) => Ok((s.clone(), vec![])),
269 EnvValueSimple::Int(i) => Ok((i.to_string(), vec![])),
270 EnvValueSimple::Bool(b) => Ok((b.to_string(), vec![])),
271 EnvValueSimple::Secret(s) => {
272 let resolved = s.resolve().await?;
273 Ok((resolved.clone(), vec![resolved]))
274 }
275 EnvValueSimple::Interpolated(parts) => Self::resolve_parts_with_secrets(parts).await,
276 }
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, Default)]
282pub struct Environment {
283 #[serde(flatten)]
285 pub vars: HashMap<String, String>,
286}
287
288impl Environment {
289 pub fn new() -> Self {
291 Self::default()
292 }
293
294 pub fn from_map(vars: HashMap<String, String>) -> Self {
296 Self { vars }
297 }
298
299 pub fn get(&self, key: &str) -> Option<&str> {
301 self.vars.get(key).map(|s| s.as_str())
302 }
303
304 pub fn set(&mut self, key: String, value: String) {
306 self.vars.insert(key, value);
307 }
308
309 pub fn contains(&self, key: &str) -> bool {
311 self.vars.contains_key(key)
312 }
313
314 pub fn to_env_vec(&self) -> Vec<String> {
316 self.vars
317 .iter()
318 .map(|(k, v)| format!("{}={}", k, v))
319 .collect()
320 }
321
322 pub fn merge_with_system(&self) -> HashMap<String, String> {
325 let mut merged: HashMap<String, String> = env::vars().collect();
326
327 for (key, value) in &self.vars {
329 merged.insert(key.clone(), value.clone());
330 }
331
332 merged
333 }
334
335 const HERMETIC_ALLOWED_VARS: &'static [&'static str] = &[
338 "HOME",
339 "USER",
340 "LOGNAME",
341 "SHELL",
342 "TERM",
343 "COLORTERM",
344 "LANG",
345 "LC_ALL",
346 "LC_CTYPE",
347 "LC_MESSAGES",
348 "TMPDIR",
349 "TMP",
350 "TEMP",
351 "XDG_RUNTIME_DIR",
352 "XDG_CONFIG_HOME",
353 "XDG_CACHE_HOME",
354 "XDG_DATA_HOME",
355 ];
356
357 pub fn merge_with_system_hermetic(&self) -> HashMap<String, String> {
369 let mut merged: HashMap<String, String> = HashMap::new();
370
371 for var in Self::HERMETIC_ALLOWED_VARS {
373 if let Ok(value) = env::var(var) {
374 merged.insert((*var).to_string(), value);
375 }
376 }
377
378 for (key, value) in env::vars() {
380 if key.starts_with("LC_") {
381 merged.insert(key, value);
382 }
383 }
384
385 for (key, value) in &self.vars {
387 merged.insert(key.clone(), value.clone());
388 }
389
390 merged
391 }
392
393 pub fn to_full_env_vec(&self) -> Vec<String> {
395 self.merge_with_system()
396 .iter()
397 .map(|(k, v)| format!("{}={}", k, v))
398 .collect()
399 }
400
401 pub fn len(&self) -> usize {
403 self.vars.len()
404 }
405
406 pub fn is_empty(&self) -> bool {
408 self.vars.is_empty()
409 }
410
411 pub fn resolve_command(&self, command: &str) -> String {
419 if command.starts_with('/') {
421 tracing::debug!(command = %command, "Command is already absolute path");
422 return command.to_string();
423 }
424
425 let path_value = self
427 .vars
428 .get("PATH")
429 .cloned()
430 .or_else(|| env::var("PATH").ok())
431 .unwrap_or_default();
432
433 tracing::debug!(
434 command = %command,
435 env_has_path = self.vars.contains_key("PATH"),
436 path_len = path_value.len(),
437 "Resolving command in PATH"
438 );
439
440 for dir in path_value.split(':') {
442 if dir.is_empty() {
443 continue;
444 }
445 let candidate = std::path::Path::new(dir).join(command);
446 if candidate.is_file() {
447 #[cfg(unix)]
449 {
450 use std::os::unix::fs::PermissionsExt;
451 if let Ok(metadata) = std::fs::metadata(&candidate) {
452 let permissions = metadata.permissions();
453 if permissions.mode() & 0o111 != 0 {
454 tracing::debug!(
455 command = %command,
456 resolved = %candidate.display(),
457 "Command resolved to path"
458 );
459 return candidate.to_string_lossy().to_string();
460 }
461 }
462 }
463 #[cfg(not(unix))]
464 {
465 tracing::debug!(
466 command = %command,
467 resolved = %candidate.display(),
468 "Command resolved to path"
469 );
470 return candidate.to_string_lossy().to_string();
471 }
472 }
473 }
474
475 if self.vars.contains_key("PATH")
479 && let Ok(system_path) = env::var("PATH")
480 {
481 tracing::debug!(
482 command = %command,
483 "Command not found in env PATH, trying system PATH"
484 );
485 for dir in system_path.split(':') {
486 if dir.is_empty() {
487 continue;
488 }
489 let candidate = std::path::Path::new(dir).join(command);
490 if candidate.is_file() {
491 #[cfg(unix)]
492 {
493 use std::os::unix::fs::PermissionsExt;
494 if let Ok(metadata) = std::fs::metadata(&candidate) {
495 let permissions = metadata.permissions();
496 if permissions.mode() & 0o111 != 0 {
497 tracing::debug!(
498 command = %command,
499 resolved = %candidate.display(),
500 "Command resolved from system PATH"
501 );
502 return candidate.to_string_lossy().to_string();
503 }
504 }
505 }
506 #[cfg(not(unix))]
507 {
508 tracing::debug!(
509 command = %command,
510 resolved = %candidate.display(),
511 "Command resolved from system PATH"
512 );
513 return candidate.to_string_lossy().to_string();
514 }
515 }
516 }
517 }
518
519 tracing::warn!(
521 command = %command,
522 env_path_set = self.vars.contains_key("PATH"),
523 "Command not found in PATH, returning original"
524 );
525 command.to_string()
526 }
527
528 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
530 self.vars.iter()
531 }
532
533 pub fn build_for_task(
535 task_name: &str,
536 env_vars: &HashMap<String, EnvValue>,
537 ) -> HashMap<String, String> {
538 env_vars
539 .iter()
540 .filter(|(_, value)| value.is_accessible_by_task(task_name))
541 .map(|(key, value)| (key.clone(), value.to_string_value()))
542 .collect()
543 }
544
545 pub async fn resolve_for_task(
550 task_name: &str,
551 env_vars: &HashMap<String, EnvValue>,
552 ) -> crate::Result<HashMap<String, String>> {
553 let (resolved, _secrets) = Self::resolve_for_task_with_secrets(task_name, env_vars).await?;
554 Ok(resolved)
555 }
556
557 pub async fn resolve_for_task_with_secrets(
564 task_name: &str,
565 env_vars: &HashMap<String, EnvValue>,
566 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
567 let mut resolved = HashMap::new();
568 let mut secrets = Vec::new();
569
570 tracing::debug!(
571 task = task_name,
572 env_count = env_vars.len(),
573 "resolve_for_task_with_secrets"
574 );
575 for (key, value) in env_vars {
576 tracing::debug!(
577 key = key,
578 is_secret = value.is_secret(),
579 accessible = value.is_accessible_by_task(task_name),
580 "checking env var"
581 );
582 if value.is_accessible_by_task(task_name) {
583 let (resolved_value, mut value_secrets) = value.resolve_with_secrets().await?;
584 if !value_secrets.is_empty() {
585 tracing::debug!(
586 key = key,
587 secret_count = value_secrets.len(),
588 "resolved secrets"
589 );
590 }
591 secrets.append(&mut value_secrets);
592 resolved.insert(key.clone(), resolved_value);
593 }
594 }
595 Ok((resolved, secrets))
596 }
597
598 pub fn build_for_exec(
600 command: &str,
601 env_vars: &HashMap<String, EnvValue>,
602 ) -> HashMap<String, String> {
603 env_vars
604 .iter()
605 .filter(|(_, value)| value.is_accessible_by_exec(command))
606 .map(|(key, value)| (key.clone(), value.to_string_value()))
607 .collect()
608 }
609
610 pub async fn resolve_for_exec(
615 command: &str,
616 env_vars: &HashMap<String, EnvValue>,
617 ) -> crate::Result<HashMap<String, String>> {
618 let (resolved, _secrets) = Self::resolve_for_exec_with_secrets(command, env_vars).await?;
619 Ok(resolved)
620 }
621
622 pub async fn resolve_for_exec_with_secrets(
629 command: &str,
630 env_vars: &HashMap<String, EnvValue>,
631 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
632 let mut resolved = HashMap::new();
633 let mut secrets = Vec::new();
634
635 for (key, value) in env_vars {
636 if value.is_accessible_by_exec(command) {
637 let (resolved_value, mut value_secrets) = value.resolve_with_secrets().await?;
638 secrets.append(&mut value_secrets);
639 resolved.insert(key.clone(), resolved_value);
640 }
641 }
642 Ok((resolved, secrets))
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
651 fn test_environment_basics() {
652 let mut env = Environment::new();
653 assert!(env.is_empty());
654
655 env.set("FOO".to_string(), "bar".to_string());
656 assert_eq!(env.len(), 1);
657 assert!(env.contains("FOO"));
658 assert_eq!(env.get("FOO"), Some("bar"));
659 assert!(!env.contains("BAR"));
660 }
661
662 #[test]
663 fn test_environment_from_map() {
664 let mut vars = HashMap::new();
665 vars.insert("KEY1".to_string(), "value1".to_string());
666 vars.insert("KEY2".to_string(), "value2".to_string());
667
668 let env = Environment::from_map(vars);
669 assert_eq!(env.len(), 2);
670 assert_eq!(env.get("KEY1"), Some("value1"));
671 assert_eq!(env.get("KEY2"), Some("value2"));
672 }
673
674 #[test]
675 fn test_environment_to_vec() {
676 let mut env = Environment::new();
677 env.set("VAR1".to_string(), "val1".to_string());
678 env.set("VAR2".to_string(), "val2".to_string());
679
680 let vec = env.to_env_vec();
681 assert_eq!(vec.len(), 2);
682 assert!(vec.contains(&"VAR1=val1".to_string()));
683 assert!(vec.contains(&"VAR2=val2".to_string()));
684 }
685
686 #[test]
687 fn test_environment_merge_with_system() {
688 let mut env = Environment::new();
689 env.set("PATH".to_string(), "/custom/path".to_string());
690 env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
691
692 let merged = env.merge_with_system();
693
694 assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
696 assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
697
698 assert!(merged.len() >= 2);
701 }
702
703 #[test]
704 fn test_environment_iteration() {
705 let mut env = Environment::new();
706 env.set("A".to_string(), "1".to_string());
707 env.set("B".to_string(), "2".to_string());
708
709 let mut count = 0;
710 for (key, value) in env.iter() {
711 assert!(key == "A" || key == "B");
712 assert!(value == "1" || value == "2");
713 count += 1;
714 }
715 assert_eq!(count, 2);
716 }
717
718 #[test]
719 fn test_env_value_types() {
720 let str_val = EnvValue::String("test".to_string());
721 let int_val = EnvValue::Int(42);
722 let bool_val = EnvValue::Bool(true);
723
724 assert_eq!(str_val, EnvValue::String("test".to_string()));
725 assert_eq!(int_val, EnvValue::Int(42));
726 assert_eq!(bool_val, EnvValue::Bool(true));
727 }
728
729 #[test]
730 fn test_policy_task_access() {
731 let simple_var = EnvValue::String("simple".to_string());
733 assert!(simple_var.is_accessible_by_task("any_task"));
734
735 let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
737 value: EnvValueSimple::String("value".to_string()),
738 policies: None,
739 });
740 assert!(no_policy_var.is_accessible_by_task("any_task"));
741
742 let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
744 value: EnvValueSimple::String("value".to_string()),
745 policies: Some(vec![]),
746 });
747 assert!(empty_policy_var.is_accessible_by_task("any_task"));
748
749 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
751 value: EnvValueSimple::String("secret".to_string()),
752 policies: Some(vec![Policy {
753 allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
754 allow_exec: None,
755 }]),
756 });
757 assert!(restricted_var.is_accessible_by_task("deploy"));
758 assert!(restricted_var.is_accessible_by_task("release"));
759 assert!(!restricted_var.is_accessible_by_task("test"));
760 assert!(!restricted_var.is_accessible_by_task("build"));
761 }
762
763 #[test]
764 fn test_policy_exec_access() {
765 let simple_var = EnvValue::String("simple".to_string());
767 assert!(simple_var.is_accessible_by_exec("bash"));
768
769 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
771 value: EnvValueSimple::String("secret".to_string()),
772 policies: Some(vec![Policy {
773 allow_tasks: None,
774 allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
775 }]),
776 });
777 assert!(restricted_var.is_accessible_by_exec("kubectl"));
778 assert!(restricted_var.is_accessible_by_exec("terraform"));
779 assert!(!restricted_var.is_accessible_by_exec("bash"));
780 assert!(!restricted_var.is_accessible_by_exec("sh"));
781 }
782
783 #[test]
784 fn test_multiple_policies() {
785 let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
787 value: EnvValueSimple::String("value".to_string()),
788 policies: Some(vec![
789 Policy {
790 allow_tasks: Some(vec!["task1".to_string()]),
791 allow_exec: None,
792 },
793 Policy {
794 allow_tasks: Some(vec!["task2".to_string()]),
795 allow_exec: Some(vec!["kubectl".to_string()]),
796 },
797 ]),
798 });
799
800 assert!(multi_policy_var.is_accessible_by_task("task1"));
802 assert!(multi_policy_var.is_accessible_by_task("task2"));
803 assert!(!multi_policy_var.is_accessible_by_task("task3"));
804
805 assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
807 assert!(!multi_policy_var.is_accessible_by_exec("bash"));
808 }
809
810 #[test]
811 fn test_to_string_value() {
812 assert_eq!(
813 EnvValue::String("test".to_string()).to_string_value(),
814 "test"
815 );
816 assert_eq!(EnvValue::Int(42).to_string_value(), "42");
817 assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
818 assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
819
820 let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
821 value: EnvValueSimple::String("policy_value".to_string()),
822 policies: Some(vec![]),
823 });
824 assert_eq!(with_policies.to_string_value(), "policy_value");
825 }
826
827 #[test]
828 fn test_build_for_task() {
829 let mut env_vars = HashMap::new();
830
831 env_vars.insert(
833 "PUBLIC".to_string(),
834 EnvValue::String("public_value".to_string()),
835 );
836
837 env_vars.insert(
839 "SECRET".to_string(),
840 EnvValue::WithPolicies(EnvVarWithPolicies {
841 value: EnvValueSimple::String("secret_value".to_string()),
842 policies: Some(vec![Policy {
843 allow_tasks: Some(vec!["deploy".to_string()]),
844 allow_exec: None,
845 }]),
846 }),
847 );
848
849 let deploy_env = Environment::build_for_task("deploy", &env_vars);
851 assert_eq!(deploy_env.len(), 2);
852 assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
853 assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
854
855 let test_env = Environment::build_for_task("test", &env_vars);
857 assert_eq!(test_env.len(), 1);
858 assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
859 assert_eq!(test_env.get("SECRET"), None);
860 }
861
862 #[test]
863 fn test_build_for_exec() {
864 let mut env_vars = HashMap::new();
865
866 env_vars.insert(
868 "PUBLIC".to_string(),
869 EnvValue::String("public_value".to_string()),
870 );
871
872 env_vars.insert(
874 "SECRET".to_string(),
875 EnvValue::WithPolicies(EnvVarWithPolicies {
876 value: EnvValueSimple::String("secret_value".to_string()),
877 policies: Some(vec![Policy {
878 allow_tasks: None,
879 allow_exec: Some(vec!["kubectl".to_string()]),
880 }]),
881 }),
882 );
883
884 let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
886 assert_eq!(kubectl_env.len(), 2);
887 assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
888 assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
889
890 let bash_env = Environment::build_for_exec("bash", &env_vars);
892 assert_eq!(bash_env.len(), 1);
893 assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
894 assert_eq!(bash_env.get("SECRET"), None);
895 }
896
897 #[test]
898 fn test_env_for_environment() {
899 let mut base = HashMap::new();
900 base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
901 base.insert(
902 "OVERRIDE_ME".to_string(),
903 EnvValue::String("original".to_string()),
904 );
905
906 let mut dev_env = HashMap::new();
907 dev_env.insert(
908 "OVERRIDE_ME".to_string(),
909 EnvValue::String("dev".to_string()),
910 );
911 dev_env.insert(
912 "DEV_VAR".to_string(),
913 EnvValue::String("development".to_string()),
914 );
915
916 let mut environments = HashMap::new();
917 environments.insert("development".to_string(), dev_env);
918
919 let env = Env {
920 base,
921 environment: Some(environments),
922 };
923
924 let dev_vars = env.for_environment("development");
925 assert_eq!(
926 dev_vars.get("BASE_VAR"),
927 Some(&EnvValue::String("base".to_string()))
928 );
929 assert_eq!(
930 dev_vars.get("OVERRIDE_ME"),
931 Some(&EnvValue::String("dev".to_string()))
932 );
933 assert_eq!(
934 dev_vars.get("DEV_VAR"),
935 Some(&EnvValue::String("development".to_string()))
936 );
937 }
938
939 #[tokio::test]
940 async fn test_resolve_plain_string() {
941 let env_val = EnvValue::String("plain_value".to_string());
942 let resolved = env_val.resolve().await.unwrap();
943 assert_eq!(resolved, "plain_value");
944 }
945
946 #[tokio::test]
947 async fn test_resolve_int() {
948 let env_val = EnvValue::Int(42);
949 let resolved = env_val.resolve().await.unwrap();
950 assert_eq!(resolved, "42");
951 }
952
953 #[tokio::test]
954 async fn test_resolve_bool() {
955 let env_val = EnvValue::Bool(true);
956 let resolved = env_val.resolve().await.unwrap();
957 assert_eq!(resolved, "true");
958 }
959
960 #[tokio::test]
961 async fn test_resolve_with_policies_plain_string() {
962 let env_val = EnvValue::WithPolicies(EnvVarWithPolicies {
963 value: EnvValueSimple::String("policy_value".to_string()),
964 policies: None,
965 });
966 let resolved = env_val.resolve().await.unwrap();
967 assert_eq!(resolved, "policy_value");
968 }
969
970 #[test]
975 fn test_env_part_literal() {
976 let part = EnvPart::Literal("hello".to_string());
977 assert!(!part.is_secret());
978 }
979
980 #[test]
981 fn test_env_part_secret() {
982 let secret = crate::secrets::Secret::new("echo".to_string(), vec!["test".to_string()]);
983 let part = EnvPart::Secret(secret);
984 assert!(part.is_secret());
985 }
986
987 #[test]
988 fn test_env_part_deserialization_literal() {
989 let json = r#""hello""#;
990 let part: EnvPart = serde_json::from_str(json).unwrap();
991 assert!(matches!(part, EnvPart::Literal(ref s) if s == "hello"));
992 assert!(!part.is_secret());
993 }
994
995 #[test]
996 fn test_env_part_deserialization_secret() {
997 let json = r#"{"resolver": "exec", "command": "echo", "args": ["test"]}"#;
998 let part: EnvPart = serde_json::from_str(json).unwrap();
999 assert!(part.is_secret());
1000 }
1001
1002 #[test]
1003 fn test_env_value_interpolated_deserialization() {
1004 let json =
1005 r#"["prefix-", {"resolver": "exec", "command": "gh", "args": ["auth", "token"]}]"#;
1006 let value: EnvValue = serde_json::from_str(json).unwrap();
1007 assert!(matches!(value, EnvValue::Interpolated(_)));
1008 assert!(value.is_secret());
1009 }
1010
1011 #[test]
1012 fn test_interpolated_is_secret_with_no_secrets() {
1013 let parts = vec![
1014 EnvPart::Literal("hello".to_string()),
1015 EnvPart::Literal("world".to_string()),
1016 ];
1017 let value = EnvValue::Interpolated(parts);
1018 assert!(!value.is_secret());
1019 }
1020
1021 #[test]
1022 fn test_interpolated_is_secret_with_secret() {
1023 let secret = crate::secrets::Secret::new("echo".to_string(), vec![]);
1024 let parts = vec![
1025 EnvPart::Literal("prefix".to_string()),
1026 EnvPart::Secret(secret),
1027 ];
1028 let value = EnvValue::Interpolated(parts);
1029 assert!(value.is_secret());
1030 }
1031
1032 #[test]
1033 fn test_interpolated_to_string_value_redacts_secrets() {
1034 let secret = crate::secrets::Secret::new(
1035 "gh".to_string(),
1036 vec!["auth".to_string(), "token".to_string()],
1037 );
1038 let parts = vec![
1039 EnvPart::Literal("access-tokens = github.com=".to_string()),
1040 EnvPart::Secret(secret),
1041 ];
1042 let value = EnvValue::Interpolated(parts);
1043 assert_eq!(value.to_string_value(), "access-tokens = github.com=*_*");
1044 }
1045
1046 #[test]
1047 fn test_interpolated_to_string_value_no_secrets() {
1048 let parts = vec![
1049 EnvPart::Literal("hello".to_string()),
1050 EnvPart::Literal("-".to_string()),
1051 EnvPart::Literal("world".to_string()),
1052 ];
1053 let value = EnvValue::Interpolated(parts);
1054 assert_eq!(value.to_string_value(), "hello-world");
1055 }
1056
1057 #[tokio::test]
1058 async fn test_resolve_with_secrets_collects_only_secret_parts() {
1059 let parts = vec![
1062 EnvPart::Literal("hello-".to_string()),
1063 EnvPart::Literal("world".to_string()),
1064 ];
1065 let value = EnvValue::Interpolated(parts);
1066 let (resolved, secrets) = value.resolve_with_secrets().await.unwrap();
1067 assert_eq!(resolved, "hello-world");
1068 assert!(secrets.is_empty()); }
1070
1071 #[tokio::test]
1072 async fn test_resolve_interpolated_concatenates_parts() {
1073 let parts = vec![
1074 EnvPart::Literal("a".to_string()),
1075 EnvPart::Literal("b".to_string()),
1076 EnvPart::Literal("c".to_string()),
1077 ];
1078 let value = EnvValue::Interpolated(parts);
1079 let resolved = value.resolve().await.unwrap();
1080 assert_eq!(resolved, "abc");
1081 }
1082
1083 #[test]
1084 fn test_interpolated_with_policies_is_secret() {
1085 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1086 let parts = vec![
1087 EnvPart::Literal("prefix".to_string()),
1088 EnvPart::Secret(secret),
1089 ];
1090
1091 let value = EnvValue::WithPolicies(EnvVarWithPolicies {
1092 value: EnvValueSimple::Interpolated(parts),
1093 policies: Some(vec![Policy {
1094 allow_tasks: Some(vec!["deploy".to_string()]),
1095 allow_exec: None,
1096 }]),
1097 });
1098
1099 assert!(value.is_secret());
1100 }
1101
1102 #[test]
1103 fn test_interpolated_with_policies_to_string_value() {
1104 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1105 let parts = vec![
1106 EnvPart::Literal("before-".to_string()),
1107 EnvPart::Secret(secret),
1108 EnvPart::Literal("-after".to_string()),
1109 ];
1110
1111 let value = EnvValue::WithPolicies(EnvVarWithPolicies {
1112 value: EnvValueSimple::Interpolated(parts),
1113 policies: None,
1114 });
1115
1116 assert_eq!(value.to_string_value(), "before-*_*-after");
1117 }
1118
1119 #[test]
1120 fn test_interpolated_accessible_by_task() {
1121 let parts = vec![EnvPart::Literal("value".to_string())];
1122 let value = EnvValue::Interpolated(parts);
1123 assert!(value.is_accessible_by_task("any_task"));
1125 }
1126
1127 #[test]
1128 fn test_extract_static_env_vars_skips_interpolated_secrets() {
1129 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1131 let parts = vec![
1132 EnvPart::Literal("prefix".to_string()),
1133 EnvPart::Secret(secret),
1134 ];
1135
1136 let mut base = HashMap::new();
1137 base.insert("PLAIN".to_string(), EnvValue::String("value".to_string()));
1138 base.insert(
1139 "INTERPOLATED_SECRET".to_string(),
1140 EnvValue::Interpolated(parts),
1141 );
1142 base.insert(
1143 "INTERPOLATED_PLAIN".to_string(),
1144 EnvValue::Interpolated(vec![
1145 EnvPart::Literal("a".to_string()),
1146 EnvPart::Literal("b".to_string()),
1147 ]),
1148 );
1149
1150 let vars: HashMap<_, _> = base
1152 .iter()
1153 .filter(|(_, v)| !v.is_secret())
1154 .map(|(k, v)| (k.clone(), v.to_string_value()))
1155 .collect();
1156
1157 assert!(vars.contains_key("PLAIN"));
1158 assert!(!vars.contains_key("INTERPOLATED_SECRET"));
1159 assert!(vars.contains_key("INTERPOLATED_PLAIN"));
1160 assert_eq!(vars.get("INTERPOLATED_PLAIN"), Some(&"ab".to_string()));
1161 }
1162
1163 #[test]
1164 fn test_env_value_simple_interpolated_deserialization() {
1165 let json = r#"["a", "b", "c"]"#;
1167 let value: EnvValueSimple = serde_json::from_str(json).unwrap();
1168 assert!(matches!(value, EnvValueSimple::Interpolated(_)));
1169 }
1170
1171 #[test]
1172 fn test_env_value_with_policies_interpolated_deserialization() {
1173 let json = r#"{
1174 "value": ["prefix-", {"resolver": "exec", "command": "gh", "args": ["auth", "token"]}],
1175 "policies": [{"allowTasks": ["deploy"]}]
1176 }"#;
1177 let value: EnvValue = serde_json::from_str(json).unwrap();
1178 assert!(matches!(value, EnvValue::WithPolicies(_)));
1179 assert!(value.is_secret());
1180 }
1181
1182 #[test]
1183 fn test_interpolated_empty_array() {
1184 let parts = vec![];
1185 let value = EnvValue::Interpolated(parts);
1186 assert_eq!(value.to_string_value(), "");
1187 assert!(!value.is_secret());
1188 }
1189
1190 #[tokio::test]
1191 async fn test_resolve_interpolated_with_actual_secret() {
1192 let secret =
1193 crate::secrets::Secret::new("echo".to_string(), vec!["secret_value".to_string()]);
1194 let parts = vec![
1195 EnvPart::Literal("prefix-".to_string()),
1196 EnvPart::Secret(secret),
1197 EnvPart::Literal("-suffix".to_string()),
1198 ];
1199 let value = EnvValue::Interpolated(parts);
1200 let (resolved, secrets) = value.resolve_with_secrets().await.unwrap();
1201
1202 assert!(resolved.contains("prefix-"));
1203 assert!(resolved.contains("secret_value"));
1204 assert!(resolved.contains("-suffix"));
1205 assert_eq!(secrets.len(), 1);
1206 }
1207}