use std::collections::HashMap;
use super::{SecretProvider, SecretRef};
use crate::error::AppResult;
use crate::playbook::types::KeychainDef;
use crate::template::TemplateRenderer;
pub async fn resolve_keychain_entry(
kc: &KeychainDef,
workload: &HashMap<String, serde_json::Value>,
provider: &dyn SecretProvider,
) -> AppResult<serde_json::Value> {
let renderer = TemplateRenderer::new();
match &kc.map {
Some(map) => {
let mut out = serde_json::Map::with_capacity(map.len());
for (key, path_template) in map {
let path = renderer.render(path_template, workload)?;
let secret = provider
.fetch(&SecretRef {
name: path,
project: None,
version: None,
})
.await?;
out.insert(key.clone(), serde_json::Value::String(secret.value));
}
Ok(serde_json::Value::Object(out))
}
None => {
let secret = provider
.fetch(&SecretRef {
name: kc.name.clone(),
project: None,
version: None,
})
.await?;
Ok(serde_json::Value::String(secret.value))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::AppError;
use crate::secrets::SecretValue;
use async_trait::async_trait;
struct FakeProvider {
values: HashMap<String, String>,
}
impl FakeProvider {
fn new(pairs: &[(&str, &str)]) -> Self {
Self {
values: pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
}
#[async_trait]
impl SecretProvider for FakeProvider {
fn provider(&self) -> &'static str {
"fake"
}
async fn fetch(&self, secret: &SecretRef) -> AppResult<SecretValue> {
self.values
.get(&secret.name)
.map(|v| SecretValue {
value: v.clone(),
version: None,
})
.ok_or_else(|| {
AppError::NotFound(format!("fake secret '{}' not found", secret.name))
})
}
}
fn workload(pairs: &[(&str, &str)]) -> HashMap<String, serde_json::Value> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), serde_json::Value::String(v.to_string())))
.collect()
}
#[tokio::test]
async fn map_entry_renders_path_template_and_assembles_object() {
let kc: KeychainDef = serde_yaml::from_str(
r#"
name: openai_token
provider: gcp
map:
api_key: "{{ openai_secret_path }}"
"#,
)
.unwrap();
let wl = workload(&[("openai_secret_path", "projects/p/secrets/openai")]);
let provider = FakeProvider::new(&[("projects/p/secrets/openai", "sk-live-123")]);
let resolved = resolve_keychain_entry(&kc, &wl, &provider).await.unwrap();
assert_eq!(resolved, serde_json::json!({ "api_key": "sk-live-123" }));
}
#[tokio::test]
async fn map_entry_with_multiple_keys_fetches_each() {
let kc: KeychainDef = serde_yaml::from_str(
r#"
name: combo
provider: gcp
map:
key_a: "projects/p/secrets/a"
key_b: "{{ b_path }}"
"#,
)
.unwrap();
let wl = workload(&[("b_path", "projects/p/secrets/b")]);
let provider = FakeProvider::new(&[
("projects/p/secrets/a", "val-a"),
("projects/p/secrets/b", "val-b"),
]);
let resolved = resolve_keychain_entry(&kc, &wl, &provider).await.unwrap();
assert_eq!(resolved["key_a"], "val-a");
assert_eq!(resolved["key_b"], "val-b");
}
#[tokio::test]
async fn map_less_entry_resolves_single_value_by_name() {
let kc: KeychainDef = serde_yaml::from_str(
r#"
name: projects/p/secrets/duffel
provider: gcp
"#,
)
.unwrap();
let provider = FakeProvider::new(&[("projects/p/secrets/duffel", "duffel-token")]);
let resolved = resolve_keychain_entry(&kc, &HashMap::new(), &provider)
.await
.unwrap();
assert_eq!(resolved, serde_json::json!("duffel-token"));
}
#[tokio::test]
async fn missing_secret_propagates_error() {
let kc: KeychainDef = serde_yaml::from_str(
r#"
name: missing
provider: gcp
map:
k: "projects/p/secrets/nope"
"#,
)
.unwrap();
let provider = FakeProvider::new(&[]);
let err = resolve_keychain_entry(&kc, &HashMap::new(), &provider)
.await
.unwrap_err();
assert!(format!("{err:?}").contains("not found"), "got: {err:?}");
}
}