durable-actors 0.9.1

Standalone regional durable-actors control plane, host, and durability runtime
use std::collections::HashMap;

use anyhow::{Context, Result, ensure};
use async_trait::async_trait;
use k8s_openapi::api::core::v1::Secret;
use kube::{Api, Client, ResourceExt};
use tokio::sync::OnceCell;

#[async_trait]
pub(crate) trait SecretResolver: Send + Sync {
    async fn resolve(&self, names: &[String]) -> Result<HashMap<String, String>>;
}

pub(crate) struct KubernetesSecrets {
    namespace: String,
    secrets: OnceCell<Api<Secret>>,
}

impl KubernetesSecrets {
    pub(crate) fn new(namespace: String) -> Self {
        Self {
            namespace,
            secrets: OnceCell::new(),
        }
    }
}

#[async_trait]
impl SecretResolver for KubernetesSecrets {
    async fn resolve(&self, names: &[String]) -> Result<HashMap<String, String>> {
        if names.is_empty() {
            return Ok(HashMap::new());
        }
        let secrets = self
            .secrets
            .get_or_try_init(|| async {
                Client::try_default()
                    .await
                    .map(|client| Api::namespaced(client, &self.namespace))
            })
            .await?;
        let loaded =
            futures_util::future::try_join_all(names.iter().map(|name| secrets.get(name))).await?;
        customer_environment(loaded)
    }
}

fn customer_environment(secrets: Vec<Secret>) -> Result<HashMap<String, String>> {
    let mut environment = HashMap::new();
    for secret in secrets {
        ensure!(
            secret
                .labels()
                .get("terse.ai/customer-secret")
                .is_some_and(|value| value == "true"),
            "secret is not marked for customer code"
        );
        for (name, value) in secret.data.unwrap_or_default() {
            ensure!(
                !environment.contains_key(&name),
                "duplicate customer environment variable {name}"
            );
            environment.insert(
                name,
                String::from_utf8(value.0).context("customer secret must be UTF-8")?,
            );
        }
    }
    Ok(environment)
}

#[cfg(test)]
#[path = "../../tests/unit/sandbox/secrets.rs"]
mod tests;