stormchaser_engine/
hcl_eval.rs1use crate::secrets::SharedSecretBackend;
2use anyhow::Result;
3use hcl::eval::{Context as HclContext, FuncDef, ParamType};
4use hcl::Value as HclValue;
5use once_cell::sync::Lazy;
6use parking_lot::RwLock;
7use serde_json::Value;
8use stormchaser_model::RunId;
9
10pub use stormchaser_model::hcl_eval::{
11 evaluate_raw_expr, evaluate_string, hcl_to_json, json_to_hcl, resolve_expressions,
12};
13
14static SECRETS_BACKEND: Lazy<RwLock<Option<SharedSecretBackend>>> = Lazy::new(|| RwLock::new(None));
15
16pub fn set_secrets_backend(backend: SharedSecretBackend) {
18 let mut lock = SECRETS_BACKEND.write();
19 *lock = Some(backend);
20}
21
22fn secret_lookup(args: hcl::eval::FuncArgs) -> Result<HclValue, String> {
23 if args.len() != 1 {
24 return Err("secret() expects exactly 1 argument".to_string());
25 }
26
27 let path_and_key_str = match &args[0] {
28 HclValue::String(s) => s.to_string(),
29 _ => return Err("secret() expects a string argument".to_string()),
30 };
31
32 let backend = {
33 let lock = SECRETS_BACKEND.read();
34 lock.clone()
35 };
36
37 if let Some(backend) = backend {
38 let handle = tokio::runtime::Handle::current();
39
40 let (path, key) = match path_and_key_str.split_once('#') {
41 Some((p, k)) => (p.to_string(), k.to_string()),
42 None => {
43 return Err(format!(
44 "Invalid secret lookup format (expected path#key): {}",
45 path_and_key_str
46 ));
47 }
48 };
49
50 let val = match handle.runtime_flavor() {
51 tokio::runtime::RuntimeFlavor::MultiThread => tokio::task::block_in_place(move || {
52 handle.block_on(async move { backend.get_secret(&path, &key).await })
53 }),
54 _ => handle.block_on(async move { backend.get_secret(&path, &key).await }),
55 };
56
57 match val {
58 Ok(val) => Ok(HclValue::String(val)),
59 Err(e) => Err(format!(
60 "Secret lookup failed for {}: {:?}",
61 path_and_key_str, e
62 )),
63 }
64 } else {
65 Err("No secrets backend configured".to_string())
66 }
67}
68
69pub fn create_context(inputs: Value, run_id: RunId, steps: Value) -> HclContext<'static> {
71 let mut ctx = HclContext::new();
72 ctx.declare_var("inputs", json_to_hcl(inputs));
73 ctx.declare_var(
74 "run",
75 json_to_hcl(serde_json::json!({"id": run_id.to_string()})),
76 );
77 ctx.declare_var("steps", json_to_hcl(steps));
78
79 ctx.declare_func("secret", FuncDef::new(secret_lookup, [ParamType::Any]));
80 crate::stdlib::register_stdlib(&mut ctx);
81
82 ctx
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::secrets::MockBackend;
89 use std::collections::HashMap;
90 use std::sync::Arc;
91
92 #[test]
93 fn test_secret_function() {
94 let mut secrets = HashMap::new();
95 secrets.insert("kv/db:password".to_string(), "secret-password".to_string());
96 let backend = Arc::new(MockBackend::new(secrets)) as SharedSecretBackend;
97 set_secrets_backend(backend);
98
99 let ctx = create_context(
100 serde_json::json!({}),
101 RunId::new_v4(),
102 serde_json::json!({}),
103 );
104
105 let rt = tokio::runtime::Builder::new_multi_thread()
106 .enable_all()
107 .build()
108 .unwrap();
109 rt.block_on(async {
110 let result = evaluate_raw_expr("secret(\"kv/db#password\")", &ctx).unwrap();
111 assert_eq!(result, serde_json::json!("secret-password"));
112 });
113 }
114}