diff --git a/README.md b/README.md index bb3d320..c07674e 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,8 @@ gman aws sts get-caller-identity - [AWS Secrets Manager](#provider-aws_secrets_manager) - [GCP Secret Manager](#provider-gcp_secret_manager) - [Azure Key Vault](#provider-azure_key_vault) -- [Run Configurations](#run-configurations) +- [Run Configurations](#run-configurations) + - [Specifying a Default Provider per Run Config](#specifying-a-default-provider-per-run-config) - [Environment Variable Secret Injection](#environment-variable-secret-injection) - [Inject Secrets via Command-Line Flags](#inject-secrets-via-command-line-flags) - [Inject Secrets into Files](#inject-secrets-into-files) @@ -404,6 +405,45 @@ will error out and report that it could not find the run config with that name. You can manually specify which run configuration to use with the `--profile` flag. Again, if no profile is found with that name, `gman` will error out. + +### Specifying a Default Provider per Run Config +All run configs also support the `provider` field, which lets you override the default provider for that specific +profile. This is useful if you have multiple providers configured and want to use a different one for a specific command +, but that provider may not be the `default_provider`, and you don't want to have to specify `--provider` on the command +line every time. + +For Example: +```yaml +default_provider: local +run_configs: + # `gman aws ...` uses the `aws` provider instead of `local` if no + # `--provider` is given. + - name: aws + # Can be overridden by explicitly specifying a `--provider` + provider: aws + secrets: + - DB_USERNAME + - DB_PASSWORD + # `gman docker ...` uses the default_provider `local` because no + # `provider` is specified. + - name: docker + secrets: + - MY_APP_API_KEY + - MY_APP_DB_PASSWORD + # `gman managarr ...` uses the `local` provider; This is useful + # if you change the default provider to something else. + - name: managarr + provider: local + secrets: + - RADARR_API_KEY + - SONARR_API_KEY + files: + - /home/user/.config/managarr/config.yml +``` + +**Important Note:** Any run config with a `provider` field can be overridden by specifying `--provider` on the command +line. + ### Environment Variable Secret Injection By default, secrets are injected as environment variables. The two required fields are `name` and `secrets`. diff --git a/src/bin/gman/cli.rs b/src/bin/gman/cli.rs index 8b3f71f..2dd1210 100644 --- a/src/bin/gman/cli.rs +++ b/src/bin/gman/cli.rs @@ -2,7 +2,6 @@ use crate::command::preview_command; use anyhow::{Context, Result, anyhow}; use futures::future::join_all; use gman::config::{Config, RunConfig}; -use gman::providers::SecretProvider; use log::{debug, error}; use regex::Regex; use std::collections::HashMap; @@ -15,7 +14,7 @@ const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}"; const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}"; pub async fn wrap_and_run_command( - secrets_provider: &mut dyn SecretProvider, + provider: Option, config: &Config, tokens: Vec, profile_name: Option, @@ -36,6 +35,8 @@ pub async 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 mut provider_config = config.extract_provider_config(provider.or(run_cfg.provider.clone()))?; + let secrets_provider = provider_config.extract_provider(); let secrets_result_futures = run_cfg .secrets .as_ref() @@ -163,7 +164,7 @@ fn generate_files_secret_injections( secrets: HashMap<&str, String>, run_config: &RunConfig, ) -> Result> { - let re = Regex::new(r"\{\{(.+)\}\}")?; + let re = Regex::new(r"\{\{(.+)}}")?; let mut results = Vec::new(); for file in run_config .files @@ -260,26 +261,6 @@ mod tests { use std::collections::HashMap; use std::ffi::OsString; - struct DummyProvider; - #[async_trait::async_trait] - impl SecretProvider for DummyProvider { - fn name(&self) -> &'static str { - "Dummy" - } - async fn get_secret(&self, key: &str) -> Result { - Ok(format!("{}_VAL", key)) - } - async fn set_secret(&self, _key: &str, _value: &str) -> Result<()> { - Ok(()) - } - async fn delete_secret(&self, _key: &str) -> Result<()> { - Ok(()) - } - async fn sync(&mut self) -> Result<()> { - Ok(()) - } - } - #[test] fn test_generate_files_secret_injections() { let mut secrets = HashMap::new(); @@ -290,6 +271,7 @@ mod tests { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["testing/SOME-secret".to_string()]), files: Some(vec![file_path.clone()]), flag: None, @@ -309,6 +291,7 @@ mod tests { fn test_parse_args_insert_and_append() { let run_config = RunConfig { name: Some("docker".into()), + provider: None, secrets: Some(vec!["api_key".into()]), files: None, flag: Some("-e".into()), @@ -347,10 +330,8 @@ mod tests { #[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) + let err = wrap_and_run_command(None, &cfg, tokens, None, true) .await .unwrap_err(); assert!(err.to_string().contains("No run profile found")); @@ -361,6 +342,7 @@ mod tests { // Create a config with a matching run profile for command "echo" let run_cfg = RunConfig { name: Some("echo".into()), + provider: None, secrets: Some(vec!["api_key".into()]), files: None, flag: None, @@ -371,13 +353,11 @@ mod tests { run_configs: Some(vec![run_cfg]), ..Config::default() }; - let mut dummy = DummyProvider; - let prov: &mut dyn SecretProvider = &mut dummy; // 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).await; + let res = wrap_and_run_command(None, &cfg, tokens, None, true).await; assert!(res.is_ok()); // Not asserting output text to keep test platform-agnostic } diff --git a/src/bin/gman/main.rs b/src/bin/gman/main.rs index 1511332..0ea0dc8 100644 --- a/src/bin/gman/main.rs +++ b/src/bin/gman/main.rs @@ -253,8 +253,7 @@ async fn main() -> Result<()> { })?; } Commands::External(tokens) => { - wrap_and_run_command(secrets_provider, &config, tokens, cli.profile, cli.dry_run) - .await?; + wrap_and_run_command(cli.provider, &config, tokens, cli.profile, cli.dry_run).await?; } Commands::Completions { shell } => { let mut cmd = Cli::command(); diff --git a/src/config.rs b/src/config.rs index 5c403e0..7dd4a16 100644 --- a/src/config.rs +++ b/src/config.rs @@ -45,6 +45,7 @@ use validator::{Validate, ValidationError}; pub struct RunConfig { #[validate(required)] pub name: Option, + pub provider: Option, #[validate(required)] pub secrets: Option>, pub files: Option>, diff --git a/tests/config_tests.rs b/tests/config_tests.rs index a14b354..7a917bb 100644 --- a/tests/config_tests.rs +++ b/tests/config_tests.rs @@ -9,6 +9,7 @@ mod tests { fn test_run_config_valid() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -23,6 +24,7 @@ mod tests { fn test_run_config_missing_name() { let run_config = RunConfig { name: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -37,6 +39,7 @@ mod tests { fn test_run_config_missing_secrets() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: None, flag: None, flag_position: None, @@ -51,6 +54,7 @@ mod tests { fn test_run_config_invalid_flag_position() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(0), @@ -65,6 +69,7 @@ mod tests { fn test_run_config_flags_or_none_all_some() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(1), @@ -79,6 +84,7 @@ mod tests { fn test_run_config_flags_or_none_all_none() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -93,6 +99,7 @@ mod tests { fn test_run_config_flags_or_none_partial_some() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: None, @@ -107,6 +114,7 @@ mod tests { fn test_run_config_flags_or_none_missing_placeholder() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(1), @@ -121,6 +129,7 @@ mod tests { fn test_run_config_flags_or_files_all_none() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -135,6 +144,7 @@ mod tests { fn test_run_config_flags_or_files_files_is_some() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -149,6 +159,7 @@ mod tests { fn test_run_config_flags_or_files_all_some() { let run_config = RunConfig { name: Some("test".to_string()), + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(1),