spin_variables/
vault.rs

1use serde::{Deserialize, Serialize};
2use spin_expressions::async_trait::async_trait;
3use spin_factors::anyhow::{self, Context as _};
4use tracing::{instrument, Level};
5use vaultrs::{
6    client::{VaultClient, VaultClientSettingsBuilder},
7    error::ClientError,
8    kv2,
9};
10
11use spin_expressions::{Key, Provider};
12
13#[derive(Debug, Default, Deserialize)]
14#[serde(deny_unknown_fields)]
15/// A config Provider that uses HashiCorp Vault.
16pub struct VaultVariablesProvider {
17    /// The URL of the Vault server.
18    url: String,
19    /// The token to authenticate with.
20    token: String,
21    /// The mount point of the KV engine.
22    mount: String,
23    /// The optional prefix to use for all keys.
24    #[serde(default)]
25    prefix: Option<String>,
26}
27
28#[async_trait]
29impl Provider for VaultVariablesProvider {
30    #[instrument(name = "spin_variables.get_from_vault", level = Level::DEBUG, skip(self), err(level = Level::INFO), fields(otel.kind = "client"))]
31    async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> {
32        let client = VaultClient::new(
33            VaultClientSettingsBuilder::default()
34                .address(&self.url)
35                .token(&self.token)
36                .build()?,
37        )?;
38        let path = match &self.prefix {
39            Some(prefix) => format!("{}/{}", prefix, key.as_str()),
40            None => key.as_str().to_string(),
41        };
42
43        #[derive(Deserialize, Serialize)]
44        struct Secret {
45            value: String,
46        }
47        match kv2::read::<Secret>(&client, &self.mount, &path).await {
48            Ok(secret) => Ok(Some(secret.value)),
49            // Vault doesn't have this entry so pass along the chain
50            Err(ClientError::APIError { code: 404, .. }) => Ok(None),
51            // Other Vault error so bail rather than looking elsewhere
52            Err(e) => Err(e).context("Failed to check Vault for config"),
53        }
54    }
55}