cuenv_secrets/resolvers/
exec.rs1use crate::{SecretError, SecretResolver, SecretSpec};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8use tokio::process::Command;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct ExecSecretConfig {
13 pub command: String,
15
16 #[serde(default)]
18 pub args: Vec<String>,
19
20 #[serde(flatten)]
22 pub extra: HashMap<String, Value>,
23}
24
25impl ExecSecretConfig {
26 #[must_use]
28 #[allow(dead_code)] pub fn new(command: impl Into<String>, args: Vec<String>) -> Self {
30 Self {
31 command: command.into(),
32 args,
33 extra: HashMap::new(),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Default)]
43pub struct ExecSecretResolver;
44
45impl ExecSecretResolver {
46 #[must_use]
48 pub const fn new() -> Self {
49 Self
50 }
51
52 async fn execute_command(
54 &self,
55 name: &str,
56 command: &str,
57 args: &[String],
58 ) -> Result<String, SecretError> {
59 let output = Command::new(command)
60 .args(args)
61 .output()
62 .await
63 .map_err(|e| SecretError::ResolutionFailed {
64 name: name.to_string(),
65 message: format!("Failed to execute command '{command}': {e}"),
66 })?;
67
68 if !output.status.success() {
69 let stderr = String::from_utf8_lossy(&output.stderr);
70 return Err(SecretError::ResolutionFailed {
71 name: name.to_string(),
72 message: format!("Command '{command}' failed: {stderr}"),
73 });
74 }
75
76 let stdout = String::from_utf8_lossy(&output.stdout);
77 Ok(stdout.trim().to_string())
78 }
79}
80
81#[async_trait]
82impl SecretResolver for ExecSecretResolver {
83 fn provider_name(&self) -> &'static str {
84 "exec"
85 }
86
87 async fn resolve(&self, name: &str, spec: &SecretSpec) -> Result<String, SecretError> {
88 if let Ok(config) = serde_json::from_str::<ExecSecretConfig>(&spec.source) {
90 return self
91 .execute_command(name, &config.command, &config.args)
92 .await;
93 }
94
95 self.execute_command(name, "sh", &["-c".to_string(), spec.source.clone()])
97 .await
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[tokio::test]
106 async fn test_exec_simple_command() {
107 let resolver = ExecSecretResolver::new();
108 let spec = SecretSpec::new("echo test_value");
109 let result = resolver.resolve("test", &spec).await;
110
111 assert_eq!(result.unwrap(), "test_value");
112 }
113
114 #[tokio::test]
115 async fn test_exec_json_config() {
116 let config = ExecSecretConfig::new("echo", vec!["json_value".to_string()]);
117 let json_source = serde_json::to_string(&config).unwrap();
118
119 let resolver = ExecSecretResolver::new();
120 let spec = SecretSpec::new(json_source);
121 let result = resolver.resolve("test", &spec).await;
122
123 assert_eq!(result.unwrap(), "json_value");
124 }
125
126 #[tokio::test]
127 async fn test_exec_command_failure() {
128 let resolver = ExecSecretResolver::new();
129 let spec = SecretSpec::new("exit 1");
130 let result = resolver.resolve("test", &spec).await;
131
132 assert!(matches!(result, Err(SecretError::ResolutionFailed { .. })));
133 }
134}