use anyhow::{Context, Result, bail};
use super::{
runtime::RuntimeConfig,
schema::{SecretOrString, SecretRef, SecretSource},
};
pub struct SecretsManager;
impl SecretsManager {
pub fn resolve_all(config: &mut RuntimeConfig) -> Result<()> {
let _ = config; Ok(())
}
pub fn resolve(
value: &SecretOrString,
context: &str,
config: &RuntimeConfig,
) -> Result<String> {
match value {
SecretOrString::Plain(s) => Ok(s.clone()),
SecretOrString::Ref(r) => resolve_ref(r, context, config),
}
}
}
fn resolve_ref(r: &SecretRef, context: &str, config: &RuntimeConfig) -> Result<String> {
match r.source {
SecretSource::Env => resolve_env(r, context),
SecretSource::File => resolve_file(r, context, config),
SecretSource::Exec => resolve_exec(r, context, config),
}
}
fn resolve_env(r: &SecretRef, context: &str) -> Result<String> {
std::env::var(&r.id).with_context(|| {
format!(
"secret resolution failed for {context}: \
env var `{}` is not set",
r.id
)
})
}
fn resolve_file(r: &SecretRef, context: &str, config: &RuntimeConfig) -> Result<String> {
let provider_name = r.provider.as_deref().unwrap_or("default");
let secrets_cfg = config.ops.secrets.as_ref().with_context(|| {
format!(
"secret resolution failed for {context}: \
secrets.providers is not configured (needed for file provider `{provider_name}`)"
)
})?;
let provider = secrets_cfg.providers.get(provider_name).with_context(|| {
format!(
"secret resolution failed for {context}: \
secrets provider `{provider_name}` not found"
)
})?;
let file_path = provider.file.as_deref().with_context(|| {
format!(
"secret resolution failed for {context}: \
secrets provider `{provider_name}` has type=file but no `file` path"
)
})?;
let raw = std::fs::read_to_string(file_path).with_context(|| {
format!(
"secret resolution failed for {context}: \
could not read secrets file `{file_path}`"
)
})?;
let pointer = &r.id;
if pointer.is_empty() || pointer == "/" {
return Ok(raw.trim().to_owned());
}
let json: serde_json::Value = serde_json::from_str(&raw).with_context(|| {
format!(
"secret resolution failed for {context}: \
secrets file `{file_path}` is not valid JSON"
)
})?;
let found = json.pointer(pointer).with_context(|| {
format!(
"secret resolution failed for {context}: \
JSON Pointer `{pointer}` not found in `{file_path}`"
)
})?;
match found {
serde_json::Value::String(s) => Ok(s.clone()),
other => Ok(other.to_string()),
}
}
fn resolve_exec(r: &SecretRef, context: &str, config: &RuntimeConfig) -> Result<String> {
let provider_name = r.provider.as_deref().unwrap_or("default");
let secrets_cfg = config.ops.secrets.as_ref().with_context(|| {
format!(
"secret resolution failed for {context}: \
secrets.providers is not configured (needed for exec provider `{provider_name}`)"
)
})?;
let provider = secrets_cfg.providers.get(provider_name).with_context(|| {
format!(
"secret resolution failed for {context}: \
secrets provider `{provider_name}` not found"
)
})?;
let command = provider.command.as_deref().with_context(|| {
format!(
"secret resolution failed for {context}: \
secrets provider `{provider_name}` has type=exec but no `command`"
)
})?;
let mut cmd = std::process::Command::new(command);
if let Some(args) = &provider.args {
cmd.args(args);
}
cmd.arg(&r.id);
let output = cmd.output().with_context(|| {
format!(
"secret resolution failed for {context}: \
could not execute secrets provider command `{command}` (provider `{provider_name}`)"
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"secret resolution failed for {context}: \
provider `{provider_name}` exited with status {}: {}",
output.status,
stderr.trim()
);
}
let value = String::from_utf8(output.stdout).with_context(|| {
format!(
"secret resolution failed for {context}: \
provider `{provider_name}` stdout is not valid UTF-8"
)
})?;
Ok(value.trim().to_owned())
}