use crate::{SecretError, SecretResolver, SecretSpec};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use tokio::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExecSecretConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
impl ExecSecretConfig {
#[must_use]
#[allow(dead_code)] pub fn new(command: impl Into<String>, args: Vec<String>) -> Self {
Self {
command: command.into(),
args,
extra: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ExecSecretResolver;
impl ExecSecretResolver {
#[must_use]
pub const fn new() -> Self {
Self
}
async fn execute_command(
&self,
name: &str,
command: &str,
args: &[String],
) -> Result<String, SecretError> {
let output = Command::new(command)
.args(args)
.output()
.await
.map_err(|e| SecretError::ResolutionFailed {
name: name.to_string(),
message: format!("Failed to execute command '{command}': {e}"),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SecretError::ResolutionFailed {
name: name.to_string(),
message: format!("Command '{command}' failed: {stderr}"),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.trim().to_string())
}
}
#[async_trait]
impl SecretResolver for ExecSecretResolver {
fn provider_name(&self) -> &'static str {
"exec"
}
async fn resolve(&self, name: &str, spec: &SecretSpec) -> Result<String, SecretError> {
if let Ok(config) = serde_json::from_str::<ExecSecretConfig>(&spec.source) {
return self
.execute_command(name, &config.command, &config.args)
.await;
}
self.execute_command(name, "sh", &["-c".to_string(), spec.source.clone()])
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_exec_simple_command() {
let resolver = ExecSecretResolver::new();
let spec = SecretSpec::new("echo test_value");
let result = resolver.resolve("test", &spec).await;
assert_eq!(result.unwrap(), "test_value");
}
#[tokio::test]
async fn test_exec_json_config() {
let config = ExecSecretConfig::new("echo", vec!["json_value".to_string()]);
let json_source = serde_json::to_string(&config).unwrap();
let resolver = ExecSecretResolver::new();
let spec = SecretSpec::new(json_source);
let result = resolver.resolve("test", &spec).await;
assert_eq!(result.unwrap(), "json_value");
}
#[tokio::test]
async fn test_exec_command_failure() {
let resolver = ExecSecretResolver::new();
let spec = SecretSpec::new("exit 1");
let result = resolver.resolve("test", &spec).await;
assert!(matches!(result, Err(SecretError::ResolutionFailed { .. })));
}
}