Skip to main content

stormchaser_engine/
hcl_eval.rs

1use 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
16/// Sets the global secrets backend used for resolving `secrets.*` references in HCL expressions.
17pub 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
69/// Create context.
70pub 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}