feat: AWS Secrets Manager support

This commit is contained in:
2025-09-12 17:11:44 -06:00
parent ae7f04a423
commit 81989f8c94
11 changed files with 344 additions and 142 deletions
+59 -54
View File
@@ -1,8 +1,8 @@
use crate::command::preview_command;
use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use futures::future::join_all;
use gman::config::{Config, RunConfig};
use gman::providers::SecretProvider;
use heck::ToSnakeCase;
use log::{debug, error};
use regex::Regex;
use std::collections::HashMap;
@@ -14,7 +14,7 @@ use std::process::Command;
const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}";
const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}";
pub fn wrap_and_run_command(
pub async fn wrap_and_run_command(
secrets_provider: &mut dyn SecretProvider,
config: &Config,
tokens: Vec<OsString>,
@@ -36,43 +36,40 @@ pub fn wrap_and_run_command(
.find(|c| c.name.as_deref() == Some(run_config_profile_name))
});
if let Some(run_cfg) = run_config_opt {
let secrets_result = run_cfg
let secrets_result_futures = run_cfg
.secrets
.as_ref()
.ok_or_else(|| {
anyhow!("No secrets configured for run profile '{run_config_profile_name}'")
})?
.iter()
.map(|key| {
let secret_name = key.to_snake_case().to_uppercase();
.map(async |key| {
debug!(
"Retrieving secret '{secret_name}' for run profile '{}'",
"Retrieving secret '{key}' for run profile '{}'",
run_config_profile_name
);
secrets_provider
.get_secret(key.to_snake_case().to_uppercase().as_str())
.ok()
.map_or_else(
|| {
debug!("Failed to fetch secret '{secret_name}' from secret provider");
(
key.to_uppercase(),
Err(anyhow!(
"Failed to fetch secret '{secret_name}' from secret provider"
)),
)
},
|value| {
if dry_run {
(key.to_uppercase(), Ok("*****".into()))
} else {
(key.to_uppercase(), Ok(value))
}
},
)
secrets_provider.get_secret(key).await.ok().map_or_else(
|| {
debug!("Failed to fetch secret '{key}' from secret provider");
(
key,
Err(anyhow!(
"Failed to fetch secret '{key}' from secret provider"
)),
)
},
|value| {
if dry_run {
(key, Ok("*****".into()))
} else {
(key, Ok(value))
}
},
)
});
let secrets_result = join_all(secrets_result_futures).await;
let err = secrets_result
.clone()
.iter()
.filter(|(_, r)| r.is_err())
.collect::<Vec<_>>();
if !err.is_empty() {
@@ -86,14 +83,15 @@ pub fn wrap_and_run_command(
));
}
let secrets = secrets_result
.map(|(k, r)| (k, r.unwrap()))
.into_iter()
.map(|(k, r)| (k.as_str(), r.unwrap()))
.collect::<HashMap<_, _>>();
let mut cmd_def = Command::new(prog);
if run_cfg.flag.is_some() {
let args = parse_args(args, run_cfg, secrets.clone(), dry_run)?;
run_cmd(cmd_def.args(&args), dry_run)?;
} else if run_cfg.files.is_some() {
let injected_files = generate_files_secret_injections(secrets.clone(), run_cfg)
let injected_files = generate_files_secret_injections(secrets, run_cfg)
.with_context(|| "failed to inject secrets into files")?;
for (file, original_content, new_content) in &injected_files {
if dry_run {
@@ -115,7 +113,7 @@ pub fn wrap_and_run_command(
e
);
debug!("Restoring original content to file '{}'", file.display());
fs::write(file, original_content) .with_context(|| format!("failed to restore original content to file '{}' after injection failure: {}", file.display(), e))?;
fs::write(file, original_content).with_context(|| format!("failed to restore original content to file '{}' after injection failure: {}", file.display(), e))?;
return Err(e);
}
}
@@ -143,7 +141,7 @@ pub fn wrap_and_run_command(
file.display()
);
debug!("Restoring original content to file '{}'", file.display());
fs::write(file, original_content) .with_context(|| format!("failed to restore original content to file '{}' after command execution failure: {}", file.display(), e))?;
fs::write(file, original_content).with_context(|| format!("failed to restore original content to file '{}' after command execution failure: {}", file.display(), e))?;
}
}
return Err(e);
@@ -162,9 +160,9 @@ pub fn wrap_and_run_command(
}
fn generate_files_secret_injections(
secrets: HashMap<String, String>,
secrets: HashMap<&str, String>,
run_config: &RunConfig,
) -> Result<Vec<(&PathBuf, String, String)>> {
) -> Result<Vec<(PathBuf, String, String)>> {
let re = Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}")?;
let mut results = Vec::new();
for file in run_config
@@ -184,12 +182,16 @@ fn generate_files_secret_injections(
})?;
let new_content = re.replace_all(&original_content, |caps: &regex::Captures| {
secrets
.get(&caps[1].to_snake_case().to_uppercase())
.get(&caps[1])
.map(|s| s.as_str())
.unwrap_or(&caps[0])
.to_string()
});
results.push((file, original_content.to_string(), new_content.to_string()));
results.push((
file.into(),
original_content.to_string(),
new_content.to_string(),
));
}
Ok(results)
}
@@ -207,7 +209,7 @@ pub fn run_cmd(cmd: &mut Command, dry_run: bool) -> Result<()> {
pub fn parse_args(
args: &[OsString],
run_config: &RunConfig,
secrets: HashMap<String, String>,
secrets: HashMap<&str, String>,
dry_run: bool,
) -> Result<Vec<OsString>> {
let mut args = args.to_vec();
@@ -259,20 +261,21 @@ mod tests {
use std::ffi::OsString;
struct DummyProvider;
#[async_trait::async_trait]
impl SecretProvider for DummyProvider {
fn name(&self) -> &'static str {
"Dummy"
}
fn get_secret(&self, key: &str) -> Result<String> {
async fn get_secret(&self, key: &str) -> Result<String> {
Ok(format!("{}_VAL", key))
}
fn set_secret(&self, _key: &str, _value: &str) -> Result<()> {
async fn set_secret(&self, _key: &str, _value: &str) -> Result<()> {
Ok(())
}
fn delete_secret(&self, _key: &str) -> Result<()> {
async fn delete_secret(&self, _key: &str) -> Result<()> {
Ok(())
}
fn sync(&mut self) -> Result<()> {
async fn sync(&mut self) -> Result<()> {
Ok(())
}
}
@@ -280,14 +283,14 @@ mod tests {
#[test]
fn test_generate_files_secret_injections() {
let mut secrets = HashMap::new();
secrets.insert("SECRET1".to_string(), "value1".to_string());
secrets.insert("SECRET1", "value1".to_string());
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "{{secret1}}").unwrap();
fs::write(&file_path, "{{SECRET1}}").unwrap();
let run_config = RunConfig {
name: Some("test".to_string()),
secrets: Some(vec!["secret1".to_string()]),
secrets: Some(vec!["SECRET1".to_string()]),
files: Some(vec![file_path.clone()]),
flag: None,
flag_position: None,
@@ -297,8 +300,8 @@ mod tests {
let result = generate_files_secret_injections(secrets, &run_config).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, &file_path);
assert_str_eq!(result[0].1, "{{secret1}}");
assert_eq!(result[0].0, file_path);
assert_str_eq!(result[0].1, "{{SECRET1}}");
assert_str_eq!(result[0].2, "value1");
}
@@ -313,7 +316,7 @@ mod tests {
arg_format: Some("{{key}}={{value}}".into()),
};
let mut secrets = HashMap::new();
secrets.insert("API_KEY".into(), "xyz".into());
secrets.insert("API_KEY", "xyz".into());
// Insert at position
let args = vec![OsString::from("run"), OsString::from("image")];
@@ -341,18 +344,20 @@ mod tests {
);
}
#[test]
fn test_wrap_and_run_command_no_profile() {
#[tokio::test]
async fn test_wrap_and_run_command_no_profile() {
let cfg = Config::default();
let mut dummy = DummyProvider;
let prov: &mut dyn SecretProvider = &mut dummy;
let tokens = vec![OsString::from("echo"), OsString::from("hi")];
let err = wrap_and_run_command(prov, &cfg, tokens, None, true).unwrap_err();
let err = wrap_and_run_command(prov, &cfg, tokens, None, true)
.await
.unwrap_err();
assert!(err.to_string().contains("No run profile found"));
}
#[test]
fn test_wrap_and_run_command_env_injection_dry_run() {
#[tokio::test]
async fn test_wrap_and_run_command_env_injection_dry_run() {
// Create a config with a matching run profile for command "echo"
let run_cfg = RunConfig {
name: Some("echo".into()),
@@ -372,7 +377,7 @@ mod tests {
// Capture stderr for dry_run preview
let tokens = vec![OsString::from("echo"), OsString::from("hello")];
// Best-effort: ensure function does not error under dry_run
let res = wrap_and_run_command(prov, &cfg, tokens, None, true);
let res = wrap_and_run_command(prov, &cfg, tokens, None, true).await;
assert!(res.is_ok());
// Not asserting output text to keep test platform-agnostic
}
+17 -18
View File
@@ -1,14 +1,13 @@
use clap::{
CommandFactory, Parser, ValueEnum, crate_authors, crate_description, crate_name, crate_version,
crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser, ValueEnum,
};
use std::ffi::OsString;
use anyhow::{Context, Result};
use clap::Subcommand;
use crossterm::execute;
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen};
use gman::config::{get_config_file_path, load_config};
use heck::ToSnakeCase;
use std::io::{self, IsTerminal, Read, Write};
use std::panic::PanicHookInfo;
@@ -143,22 +142,22 @@ async fn main() -> Result<()> {
Commands::Add { name } => {
let plaintext =
read_all_stdin().with_context(|| "unable to read plaintext from stdin")?;
let snake_case_name = name.to_snake_case().to_uppercase();
secrets_provider
.set_secret(&snake_case_name, plaintext.trim_end())
.set_secret(&name, plaintext.trim_end())
.await
.map(|_| match cli.output {
Some(_) => (),
None => println!("✓ Secret '{snake_case_name}' added to the vault."),
None => println!("✓ Secret '{name}' added to the vault."),
})?;
}
Commands::Get { name } => {
let snake_case_name = name.to_snake_case().to_uppercase();
secrets_provider
.get_secret(&snake_case_name)
.get_secret(&name)
.await
.map(|secret| match cli.output {
Some(OutputFormat::Json) => {
let json_output = serde_json::json!({
snake_case_name: secret
name: secret
});
println!(
"{}",
@@ -174,24 +173,23 @@ async fn main() -> Result<()> {
Commands::Update { name } => {
let plaintext =
read_all_stdin().with_context(|| "unable to read plaintext from stdin")?;
let snake_case_name = name.to_snake_case().to_uppercase();
secrets_provider
.update_secret(&snake_case_name, plaintext.trim_end())
.update_secret(&name, plaintext.trim_end())
.await
.map(|_| match cli.output {
Some(_) => (),
None => println!("✓ Secret '{snake_case_name}' updated in the vault."),
None => println!("✓ Secret '{name}' updated in the vault."),
})?;
}
Commands::Delete { name } => {
let snake_case_name = name.to_snake_case().to_uppercase();
secrets_provider.delete_secret(&snake_case_name).map(|_| {
secrets_provider.delete_secret(&name).await.map(|_| {
if cli.output.is_none() {
println!("✓ Secret '{snake_case_name}' deleted from the vault.")
println!("✓ Secret '{name}' deleted from the vault.")
}
})?;
}
Commands::List {} => {
let secrets = secrets_provider.list_secrets()?;
let secrets = secrets_provider.list_secrets().await?;
if secrets.is_empty() {
match cli.output {
Some(OutputFormat::Json) => {
@@ -217,14 +215,15 @@ async fn main() -> Result<()> {
}
}
Commands::Sync {} => {
secrets_provider.sync().map(|_| {
secrets_provider.sync().await.map(|_| {
if cli.output.is_none() {
println!("✓ Secrets synchronized with remote")
}
})?;
}
Commands::External(tokens) => {
wrap_and_run_command(secrets_provider, &config, tokens, cli.profile, cli.dry_run)?;
wrap_and_run_command(secrets_provider, &config, tokens, cli.profile, cli.dry_run)
.await?;
}
Commands::Completions { shell } => {
let mut cmd = Cli::command();
+14 -10
View File
@@ -136,8 +136,9 @@ impl ProviderConfig {
///
/// ```no_run
/// # use gman::config::ProviderConfig;
/// let provider_config = ProviderConfig::default().extract_provider();
/// println!("using provider: {}", provider_config.name());
/// let mut provider_config = ProviderConfig::default();
/// let provider = provider_config.extract_provider();
/// println!("using provider: {}", provider.name());
/// ```
pub fn extract_provider(&mut self) -> &mut dyn SecretProvider {
match &mut self.provider_type {
@@ -145,6 +146,10 @@ impl ProviderConfig {
debug!("Using local secret provider");
provider_def
}
SupportedProvider::AwsSecretsManager { provider_def } => {
debug!("Using AWS Secrets Manager provider");
provider_def
}
}
}
}
@@ -278,15 +283,14 @@ pub fn load_config() -> Result<Config> {
.providers
.iter_mut()
.filter(|p| matches!(p.provider_type, SupportedProvider::Local { .. }))
.for_each(|p| match p.provider_type {
SupportedProvider::Local {
.for_each(|p| {
if let SupportedProvider::Local {
ref mut provider_def,
} => {
if provider_def.password_file.is_none()
&& let Some(local_password_file) = Config::local_provider_password_file()
{
provider_def.password_file = Some(local_password_file);
}
} = p.provider_type
&& provider_def.password_file.is_none()
&& let Some(local_password_file) = Config::local_provider_password_file()
{
provider_def.password_file = Some(local_password_file);
}
});
+124
View File
@@ -0,0 +1,124 @@
use crate::providers::SecretProvider;
use anyhow::Context;
use anyhow::Result;
use aws_config::Region;
use aws_sdk_secretsmanager::Client;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use validator::Validate;
#[skip_serializing_none]
/// Configuration for AWS Secrets Manager provider
/// See [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/)
/// for more information.
///
/// This provider stores secrets in AWS Secrets Manager. It requires
/// AWS credentials to be configured in the AWS configuration
/// files for different AWS profiles.
///
/// Example
/// ```no_run
/// use gman::providers::{SecretProvider, SupportedProvider};
/// use gman::config::{Config, ProviderConfig};
/// use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider;
///
/// let provider = AwsSecretsManagerProvider {
/// aws_profile: Some("prod".to_string()),
/// aws_region: Some("us-west-2".to_string()),
/// };
/// let _ = provider.set_secret("MY_SECRET", "value");
/// ```
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct AwsSecretsManagerProvider {
#[validate(required)]
pub aws_profile: Option<String>,
#[validate(required)]
pub aws_region: Option<String>,
}
#[async_trait::async_trait]
impl SecretProvider for AwsSecretsManagerProvider {
fn name(&self) -> &'static str {
"AwsSecretsManagerProvider"
}
async fn get_secret(&self, key: &str) -> Result<String> {
self.get_client()
.await?
.get_secret_value()
.secret_id(key)
.send()
.await?
.secret_string
.with_context(|| format!("Secret '{key}' not found"))
}
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
self.get_client()
.await?
.create_secret()
.name(key)
.secret_string(value)
.send()
.await.with_context(|| format!("Failed to set secret '{key}'"))?;
Ok(())
}
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
self.get_client()
.await?
.update_secret()
.secret_id(key)
.secret_string(value)
.send()
.await.with_context(|| format!("Failed to update secret '{key}'"))?;
Ok(())
}
async fn delete_secret(&self, key: &str) -> Result<()> {
self.get_client()
.await?
.delete_secret()
.secret_id(key)
.force_delete_without_recovery(true)
.send()
.await
.with_context(|| format!("Failed to delete secret '{key}'"))?;
Ok(())
}
async fn list_secrets(&self) -> Result<Vec<String>> {
self.get_client()
.await?
.list_secrets()
.send()
.await?
.secret_list
.with_context(|| "No secrets found")
.map(|secrets| secrets.into_iter().filter_map(|s| s.name).collect())
}
}
impl AwsSecretsManagerProvider {
async fn get_client(&self) -> Result<Client> {
let region = self
.aws_region
.clone()
.with_context(|| "aws_region is required")?;
let profile = self
.aws_profile
.clone()
.with_context(|| "aws_profile is required")?;
let config = aws_config::from_env()
.region(Region::new(region))
.profile_name(profile)
.load()
.await;
Ok(Client::new(&config))
}
}
+15 -16
View File
@@ -1,4 +1,4 @@
use anyhow::{Context, anyhow, bail};
use anyhow::{anyhow, bail, Context};
use secrecy::{ExposeSecret, SecretString};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@@ -6,20 +6,20 @@ use std::{env, fs};
use zeroize::Zeroize;
use crate::config::Config;
use crate::providers::git_sync::{repo_name_from_url, sync_and_push, SyncOpts};
use crate::providers::SecretProvider;
use crate::providers::git_sync::{SyncOpts, repo_name_from_url, sync_and_push};
use crate::{
ARGON_M_COST_KIB, ARGON_P, ARGON_T_COST, HEADER, KDF, KEY_LEN, NONCE_LEN, SALT_LEN, VERSION,
};
use anyhow::Result;
use argon2::{Algorithm, Argon2, Params, Version};
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use chacha20poly1305::aead::rand_core::RngCore;
use chacha20poly1305::{
Key, XChaCha20Poly1305, XNonce,
aead::{Aead, KeyInit, OsRng},
Key, XChaCha20Poly1305, XNonce,
};
use dialoguer::{Input, theme};
use dialoguer::{theme, Input};
use log::{debug, error};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
@@ -36,15 +36,13 @@ use validator::Validate;
/// Example
/// ```no_run
/// use gman::providers::local::LocalProvider;
/// use gman::providers::SecretProvider;
/// use gman::config::Config;
/// use gman::providers::{SecretProvider, SupportedProvider};
/// use gman::config::{Config, ProviderConfig};
///
/// let provider = LocalProvider::default();
/// let cfg = Config::default();
/// // Will prompt for a password when reading/writing secrets unless a
/// // password file is configured.
/// // provider.set_secret(&cfg, "MY_SECRET", "value")?;
/// # Ok::<(), anyhow::Error>(())
/// let _ = provider.set_secret("MY_SECRET", "value");
/// ```
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
@@ -71,12 +69,13 @@ impl Default for LocalProvider {
}
}
#[async_trait::async_trait]
impl SecretProvider for LocalProvider {
fn name(&self) -> &'static str {
"LocalProvider"
}
fn get_secret(&self, key: &str) -> Result<String> {
async fn get_secret(&self, key: &str) -> Result<String> {
let vault_path = self.active_vault_path()?;
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
let envelope = vault
@@ -90,7 +89,7 @@ impl SecretProvider for LocalProvider {
Ok(plaintext)
}
fn set_secret(&self, key: &str, value: &str) -> Result<()> {
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
let vault_path = self.active_vault_path()?;
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
if vault.contains_key(key) {
@@ -109,7 +108,7 @@ impl SecretProvider for LocalProvider {
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
}
fn update_secret(&self, key: &str, value: &str) -> Result<()> {
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
let vault_path = self.active_vault_path()?;
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
@@ -132,7 +131,7 @@ impl SecretProvider for LocalProvider {
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
}
fn delete_secret(&self, key: &str) -> Result<()> {
async fn delete_secret(&self, key: &str) -> Result<()> {
let vault_path = self.active_vault_path()?;
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
if !vault.contains_key(key) {
@@ -144,7 +143,7 @@ impl SecretProvider for LocalProvider {
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
}
fn list_secrets(&self) -> Result<Vec<String>> {
async fn list_secrets(&self) -> Result<Vec<String>> {
let vault_path = self.active_vault_path()?;
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
let keys: Vec<String> = vault.keys().cloned().collect();
@@ -152,7 +151,7 @@ impl SecretProvider for LocalProvider {
Ok(keys)
}
fn sync(&mut self) -> Result<()> {
async fn sync(&mut self) -> Result<()> {
let mut config_changed = false;
if self.git_branch.is_none() {
+23 -18
View File
@@ -2,43 +2,42 @@
//!
//! Implementations provide storage/backends for secrets and a common
//! interface used by the CLI.
pub mod aws_secrets_manager;
mod git_sync;
pub mod local;
use crate::providers::local::LocalProvider;
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use thiserror::Error;
use validator::{Validate, ValidationErrors};
/// A secret storage backend capable of CRUD and sync, with optional
/// update and listing
pub trait SecretProvider {
/// A secret storage backend capable of CRUD, with optional
/// update, listing, and sync support.
#[async_trait::async_trait]
pub trait SecretProvider: Send + Sync {
fn name(&self) -> &'static str;
fn get_secret(&self, key: &str) -> Result<String>;
fn set_secret(&self, key: &str, value: &str) -> Result<()>;
fn update_secret(&self, _key: &str, _value: &str) -> Result<()> {
async fn get_secret(&self, key: &str) -> Result<String>;
async fn set_secret(&self, key: &str, value: &str) -> Result<()>;
async fn update_secret(&self, _key: &str, _value: &str) -> Result<()> {
Err(anyhow!(
"update secret not supported for provider {}",
self.name()
))
}
fn delete_secret(&self, key: &str) -> Result<()>;
fn list_secrets(&self) -> Result<Vec<String>> {
async fn delete_secret(&self, key: &str) -> Result<()>;
async fn list_secrets(&self) -> Result<Vec<String>> {
Err(anyhow!(
"list secrets is not supported for the provider {}",
self.name()
))
}
fn sync(&mut self) -> Result<()>;
}
/// Errors when parsing a provider identifier.
#[derive(Debug, Error)]
pub enum ParseProviderError {
#[error("unsupported provider '{0}'")]
Unsupported(String),
async fn sync(&mut self) -> Result<()> {
Err(anyhow!(
"sync is not supported for the provider {}",
self.name()
))
}
}
/// Registry of built-in providers.
@@ -50,12 +49,17 @@ pub enum SupportedProvider {
#[serde(flatten)]
provider_def: LocalProvider,
},
AwsSecretsManager {
#[serde(flatten)]
provider_def: aws_secrets_manager::AwsSecretsManagerProvider,
},
}
impl Validate for SupportedProvider {
fn validate(&self) -> Result<(), ValidationErrors> {
match self {
SupportedProvider::Local { provider_def } => provider_def.validate(),
SupportedProvider::AwsSecretsManager { provider_def } => provider_def.validate(),
}
}
}
@@ -72,6 +76,7 @@ impl Display for SupportedProvider {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SupportedProvider::Local { .. } => write!(f, "local"),
SupportedProvider::AwsSecretsManager { .. } => write!(f, "aws_secrets_manager"),
}
}
}