Skip to main content

tatara_engine/secrets/
sops.rs

1//! SOPS secret provider — decrypts secrets from SOPS-encrypted files.
2//! Used for local development with encrypted secret files.
3
4use super::{SecretFetcher, SecretValue};
5use anyhow::{Context, Result};
6use async_trait::async_trait;
7use tokio::process::Command;
8
9pub struct SopsSecretFetcher;
10
11#[async_trait]
12impl SecretFetcher for SopsSecretFetcher {
13    /// Fetch a secret from a SOPS-encrypted file.
14    ///
15    /// Key format: `path/to/file.yaml#key.subkey`
16    /// Runs: `sops --decrypt --extract '["key"]["subkey"]' path/to/file.yaml`
17    async fn fetch(&self, key: &str) -> Result<SecretValue> {
18        let (file_path, json_path) = key
19            .split_once('#')
20            .context("SOPS key must be 'file_path#json.path'")?;
21
22        // Convert dot path to SOPS extract format: key.subkey -> ["key"]["subkey"]
23        let extract_path = json_path
24            .split('.')
25            .map(|part| format!("[\"{part}\"]"))
26            .collect::<String>();
27
28        let output = Command::new("sops")
29            .args(["--decrypt", "--extract", &extract_path, file_path])
30            .output()
31            .await
32            .context("failed to run sops")?;
33
34        if !output.status.success() {
35            let stderr = String::from_utf8_lossy(&output.stderr);
36            anyhow::bail!("sops decrypt failed: {stderr}");
37        }
38
39        let value = String::from_utf8(output.stdout)
40            .context("sops output not valid UTF-8")?
41            .trim()
42            .to_string();
43
44        Ok(SecretValue {
45            value,
46            version: "sops".to_string(),
47        })
48    }
49}