stormchaser-engine 0.1.0

A robust, distributed workflow engine for event-driven and human-triggered workflows.
Documentation
use crate::secrets::SharedSecretBackend;
use anyhow::Result;
use hcl::eval::{Context as HclContext, FuncDef, ParamType};
use hcl::Value as HclValue;
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use serde_json::Value;
use uuid::Uuid;

pub use stormchaser_model::hcl_eval::{
    evaluate_raw_expr, evaluate_string, hcl_to_json, json_to_hcl, resolve_expressions,
};

static SECRETS_BACKEND: Lazy<RwLock<Option<SharedSecretBackend>>> = Lazy::new(|| RwLock::new(None));

/// Sets the global secrets backend used for resolving `secrets.*` references in HCL expressions.
pub fn set_secrets_backend(backend: SharedSecretBackend) {
    let mut lock = SECRETS_BACKEND.write();
    *lock = Some(backend);
}

fn secret_lookup(args: hcl::eval::FuncArgs) -> Result<HclValue, String> {
    if args.len() != 1 {
        return Err("secret() expects exactly 1 argument".to_string());
    }

    let path_and_key_str = match &args[0] {
        HclValue::String(s) => s.to_string(),
        _ => return Err("secret() expects a string argument".to_string()),
    };

    let backend = {
        let lock = SECRETS_BACKEND.read();
        lock.clone()
    };

    if let Some(backend) = backend {
        let handle = tokio::runtime::Handle::current();

        let (path, key) = match path_and_key_str.split_once('#') {
            Some((p, k)) => (p.to_string(), k.to_string()),
            None => {
                return Err(format!(
                    "Invalid secret lookup format (expected path#key): {}",
                    path_and_key_str
                ));
            }
        };

        let val = match handle.runtime_flavor() {
            tokio::runtime::RuntimeFlavor::MultiThread => tokio::task::block_in_place(move || {
                handle.block_on(async move { backend.get_secret(&path, &key).await })
            }),
            _ => handle.block_on(async move { backend.get_secret(&path, &key).await }),
        };

        match val {
            Ok(val) => Ok(HclValue::String(val)),
            Err(e) => Err(format!(
                "Secret lookup failed for {}: {:?}",
                path_and_key_str, e
            )),
        }
    } else {
        Err("No secrets backend configured".to_string())
    }
}

/// Create context.
pub fn create_context(inputs: Value, run_id: Uuid, steps: Value) -> HclContext<'static> {
    let mut ctx = HclContext::new();
    ctx.declare_var("inputs", json_to_hcl(inputs));
    ctx.declare_var(
        "run",
        json_to_hcl(serde_json::json!({"id": run_id.to_string()})),
    );
    ctx.declare_var("steps", json_to_hcl(steps));

    ctx.declare_func("secret", FuncDef::new(secret_lookup, [ParamType::Any]));

    ctx
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::secrets::MockBackend;
    use std::collections::HashMap;
    use std::sync::Arc;

    #[test]
    fn test_secret_function() {
        let mut secrets = HashMap::new();
        secrets.insert("kv/db:password".to_string(), "secret-password".to_string());
        let backend = Arc::new(MockBackend::new(secrets)) as SharedSecretBackend;
        set_secrets_backend(backend);

        let ctx = create_context(serde_json::json!({}), Uuid::new_v4(), serde_json::json!({}));

        let rt = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .unwrap();
        rt.block_on(async {
            let result = evaluate_raw_expr("secret(\"kv/db#password\")", &ctx).unwrap();
            assert_eq!(result, serde_json::json!("secret-password"));
        });
    }
}