diff --git a/Cargo.lock b/Cargo.lock index d9769241d9..496b7ad6c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7722,6 +7722,7 @@ dependencies = [ "tokio", "toml 0.8.14", "tracing", + "vaultrs", ] [[package]] diff --git a/crates/factor-variables/Cargo.toml b/crates/factor-variables/Cargo.toml index ac7370d709..85f0687b18 100644 --- a/crates/factor-variables/Cargo.toml +++ b/crates/factor-variables/Cargo.toml @@ -13,6 +13,7 @@ spin-world = { path = "../world" } tokio = { version = "1", features = ["rt-multi-thread"] } toml = "0.8" tracing = { workspace = true } +vaultrs = "0.6.2" [dev-dependencies] spin-factors-test = { path = "../factors-test" } diff --git a/crates/factor-variables/src/spin_cli/mod.rs b/crates/factor-variables/src/spin_cli/mod.rs index 2b6a48c481..49ea1261cd 100644 --- a/crates/factor-variables/src/spin_cli/mod.rs +++ b/crates/factor-variables/src/spin_cli/mod.rs @@ -2,9 +2,11 @@ mod env; mod statik; +mod vault; pub use env::*; pub use statik::*; +pub use vault::*; use serde::Deserialize; use spin_expressions::Provider; @@ -35,6 +37,8 @@ pub fn runtime_config_from_toml(table: &toml::Table) -> anyhow::Result Box::new(provider), } } } diff --git a/crates/factor-variables/src/spin_cli/vault.rs b/crates/factor-variables/src/spin_cli/vault.rs new file mode 100644 index 0000000000..9008ce3c5f --- /dev/null +++ b/crates/factor-variables/src/spin_cli/vault.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; +use spin_expressions::async_trait::async_trait; +use spin_factors::anyhow::{self, Context as _}; +use tracing::{instrument, Level}; +use vaultrs::{ + client::{VaultClient, VaultClientSettingsBuilder}, + error::ClientError, + kv2, +}; + +use spin_expressions::{Key, Provider}; + +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +/// A config Provider that uses HashiCorp Vault. +pub struct VaultVariablesProvider { + /// The URL of the Vault server. + url: String, + /// The token to authenticate with. + token: String, + /// The mount point of the KV engine. + mount: String, + /// The optional prefix to use for all keys. + #[serde(default)] + prefix: Option, +} + +#[async_trait] +impl Provider for VaultVariablesProvider { + #[instrument(name = "spin_variables.get_from_vault", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))] + async fn get(&self, key: &Key) -> anyhow::Result> { + let client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address(&self.url) + .token(&self.token) + .build()?, + )?; + let path = match &self.prefix { + Some(prefix) => format!("{}/{}", prefix, key.as_str()), + None => key.as_str().to_string(), + }; + + #[derive(Deserialize, Serialize)] + struct Secret { + value: String, + } + match kv2::read::(&client, &self.mount, &path).await { + Ok(secret) => Ok(Some(secret.value)), + // Vault doesn't have this entry so pass along the chain + Err(ClientError::APIError { code: 404, .. }) => Ok(None), + // Other Vault error so bail rather than looking elsewhere + Err(e) => Err(e).context("Failed to check Vault for config"), + } + } +}