From 78d7e90e68c49386809459e2f9fb009fae845356 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 30 Sep 2025 11:10:20 -0600 Subject: [PATCH] feat: Environment variable interpolation in the Gman configuration file --- Cargo.lock | 41 +++ Cargo.toml | 2 +- README.md | 25 ++ src/bin/gman/cli.rs | 153 +++++++++-- src/config.rs | 397 ++++++++++++++++++++++++++- src/providers/aws_secrets_manager.rs | 3 + src/providers/azure_key_vault.rs | 2 + src/providers/gcp_secret_manager.rs | 2 + src/providers/gopass.rs | 2 + src/providers/local.rs | 9 + 10 files changed, 615 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4612b6c..e9582d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,6 +1672,7 @@ dependencies = [ "serde_json", "serde_with", "serde_yaml", + "serial_test", "tempfile", "tokio", "validator", @@ -3251,6 +3252,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.28" @@ -3300,6 +3310,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "secrecy" version = "0.10.3" @@ -3481,6 +3497,31 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha2" version = "0.10.9" diff --git a/Cargo.toml b/Cargo.toml index ebb8664..e251b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ pretty_assertions = "1.4.1" proptest = "1.5.0" assert_cmd = "2.0.16" predicates = "3.1.2" - +serial_test = "3.2.0" [[bin]] bench = false diff --git a/README.md b/README.md index b304e7f..f3bcc65 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,31 @@ providers: run_configs: [] ``` +### Environment Variable Interpolation +The config file supports environment variable interpolation using `${VAR_NAME}` syntax. For example, to use an +AWS profile from your environment: + +```yaml +providers: + - name: aws + type: aws_secrets_manager + aws_profile: ${AWS_PROFILE} # Uses the AWS_PROFILE env var + aws_region: us-east-1 +``` + +Or to set a default profile to use when `AWS_PROFILE` is unset: + +```yaml +providers: + - name: aws + type: aws_secrets_manager + aws_profile: ${AWS_PROFILE:-default} # Uses 'default' if AWS_PROFILE is unset + aws_region: us-east-1 +``` + +**Important Note:** Environment variable interpolation is only supported in string or numeric fields. It is not +supported in lists or maps. + ## Providers `gman` supports multiple providers for secret storage. The default provider is `local`, which stores secrets in an encrypted file on your filesystem. The CLI and config format are designed to be extensible so new providers can be diff --git a/src/bin/gman/cli.rs b/src/bin/gman/cli.rs index ed09b8a..ecfb1b5 100644 --- a/src/bin/gman/cli.rs +++ b/src/bin/gman/cli.rs @@ -281,24 +281,21 @@ pub fn run_config_completer(current: &OsStr) -> Vec { } pub fn provider_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - match load_config() { - Ok(config) => { - config.providers - .iter() - .filter(|pc| { - pc.name - .as_ref() - .expect("run config has no name") - .starts_with(&*cur) - }) - .map(|pc| { - CompletionCandidate::new(pc.name.as_ref().expect("provider has no name")) - }) - .collect() - } - Err(_) => vec![], - } + let cur = current.to_string_lossy(); + match load_config() { + Ok(config) => config + .providers + .iter() + .filter(|pc| { + pc.name + .as_ref() + .expect("run config has no name") + .starts_with(&*cur) + }) + .map(|pc| CompletionCandidate::new(pc.name.as_ref().expect("provider has no name"))) + .collect(), + Err(_) => vec![], + } } pub fn secrets_completer(current: &OsStr) -> Vec { @@ -328,8 +325,11 @@ mod tests { use crate::cli::generate_files_secret_injections; use gman::config::{Config, RunConfig}; use pretty_assertions::{assert_eq, assert_str_eq}; + use serial_test::serial; use std::collections::HashMap; + use std::env as std_env; use std::ffi::OsString; + use tempfile::tempdir; #[test] fn test_generate_files_secret_injections() { @@ -430,4 +430,121 @@ mod tests { .expect_err("expected failed secret resolution in dry_run"); assert!(err.to_string().contains("Failed to fetch")); } + + #[test] + #[serial] + fn test_run_config_completer_filters_by_prefix() { + let td = tempdir().unwrap(); + let xdg = td.path().join("xdg"); + let app_dir = xdg.join("gman"); + fs::create_dir_all(&app_dir).unwrap(); + unsafe { std_env::set_var("XDG_CONFIG_HOME", &xdg) }; + + let yaml = indoc::indoc! { + "--- + default_provider: local + providers: + - name: local + type: local + run_configs: + - name: echo + secrets: [API_KEY] + - name: docker + secrets: [DB_PASSWORD] + - name: aws + secrets: [AWS_ACCESS_KEY_ID] + " + }; + fs::write(app_dir.join("config.yml"), yaml).unwrap(); + + let out = run_config_completer(OsStr::new("do")); + assert_eq!(out.len(), 1); + // Compare via debug string to avoid depending on crate internals + let rendered = format!("{:?}", &out[0]); + assert!(rendered.contains("docker"), "got: {}", rendered); + + unsafe { std_env::remove_var("XDG_CONFIG_HOME") }; + } + + #[test] + #[serial] + fn test_provider_completer_lists_matching_providers() { + let td = tempdir().unwrap(); + let xdg = td.path().join("xdg"); + let app_dir = xdg.join("gman"); + fs::create_dir_all(&app_dir).unwrap(); + unsafe { std_env::set_var("XDG_CONFIG_HOME", &xdg) }; + + let yaml = indoc::indoc! { + "--- + default_provider: local + providers: + - name: local + type: local + - name: prod + type: local + run_configs: + - name: echo + secrets: [API_KEY] + " + }; + fs::write(app_dir.join("config.yml"), yaml).unwrap(); + + // Prefix 'p' should match only 'prod' + let out = provider_completer(OsStr::new("p")); + assert_eq!(out.len(), 1); + let rendered = format!("{:?}", &out[0]); + assert!(rendered.contains("prod"), "got: {}", rendered); + + // Empty prefix returns at least both providers + let out_all = provider_completer(OsStr::new("")); + assert!(out_all.len() >= 2); + + unsafe { std_env::remove_var("XDG_CONFIG_HOME") }; + } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn test_secrets_completer_filters_keys_by_prefix() { + let td = tempdir().unwrap(); + let xdg = td.path().join("xdg"); + let app_dir = xdg.join("gman"); + fs::create_dir_all(&app_dir).unwrap(); + unsafe { std_env::set_var("XDG_CONFIG_HOME", &xdg) }; + + let yaml = indoc::indoc! { + "--- + default_provider: local + providers: + - name: local + type: local + run_configs: + - name: echo + secrets: [API_KEY] + " + }; + fs::write(app_dir.join("config.yml"), yaml).unwrap(); + + // Seed a minimal vault with keys (values are irrelevant for listing) + let vault_yaml = indoc::indoc! { + "--- + API_KEY: dummy + DB_PASSWORD: dummy + AWS_ACCESS_KEY_ID: dummy + " + }; + fs::write(app_dir.join("vault.yml"), vault_yaml).unwrap(); + + let out = secrets_completer(OsStr::new("AWS")); + assert_eq!(out.len(), 1); + let rendered = format!("{:?}", &out[0]); + assert!(rendered.contains("AWS_ACCESS_KEY_ID"), "got: {}", rendered); + + let out2 = secrets_completer(OsStr::new("DB_")); + assert_eq!(out2.len(), 1); + let rendered2 = format!("{:?}", &out2[0]); + assert!(rendered2.contains("DB_PASSWORD"), "got: {}", rendered2); + + unsafe { std_env::remove_var("XDG_CONFIG_HOME") }; + } } diff --git a/src/config.rs b/src/config.rs index d3ca4e9..2097c01 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,6 +26,7 @@ use crate::providers::{SecretProvider, SupportedProvider}; use anyhow::{Context, Result}; use collections::HashSet; use log::debug; +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use serde_with::skip_serializing_none; @@ -45,14 +46,19 @@ use validator::{Validate, ValidationError}; #[validate(schema(function = "flags_or_files"))] pub struct RunConfig { #[validate(required)] + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub name: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub provider: Option, #[validate(required)] pub secrets: Option>, pub files: Option>, + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub flag: Option, #[validate(range(min = 1))] + #[serde(default, deserialize_with = "deserialize_optional_usize_env_var")] pub flag_position: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub arg_format: Option, } @@ -192,6 +198,7 @@ impl ProviderConfig { #[validate(schema(function = "default_provider_exists"))] #[validate(schema(function = "providers_names_are_unique"))] pub struct Config { + #[serde(deserialize_with = "deserialize_optional_env_var")] pub default_provider: Option, #[validate(length(min = 1))] #[validate(nested)] @@ -331,7 +338,7 @@ pub fn load_config() -> Result { Ok(config) } -/// Returns the configuration file path that `confy` will use for this app. +/// Returns the configuration file path that `confy` will use pub fn get_config_file_path() -> Result { if let Some(base) = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from) { let dir = base.join("gman"); @@ -340,8 +347,394 @@ pub fn get_config_file_path() -> Result { if yml.exists() || yaml.exists() { return Ok(if yml.exists() { yml } else { yaml }); } - // Prefer .yml if creating anew return Ok(dir.join("config.yml")); } Ok(confy::get_configuration_file_path("gman", "config")?) } + +pub fn deserialize_optional_env_var<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + match s { + Some(value) => { + let interpolated = interpolate_env_vars(&value); + Ok(Some(interpolated)) + } + None => Ok(None), + } +} + +pub fn deserialize_optional_pathbuf_env_var<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + match s { + Some(value) => { + let interpolated = interpolate_env_vars(&value); + Ok(Some(interpolated.parse().unwrap())) + } + None => Ok(None), + } +} + +fn deserialize_optional_usize_env_var<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + match s { + Some(value) => { + let interpolated = interpolate_env_vars(&value); + interpolated + .parse::() + .map(Some) + .map_err(serde::de::Error::custom) + } + None => Ok(None), + } +} + +pub fn interpolate_env_vars(s: &str) -> String { + let result = s.to_string(); + let scrubbing_regex = Regex::new(r#"[\s{}^()\[\]\\|`'"]+"#).unwrap(); + let var_regex = Regex::new(r"\$\{(.*?)(:-.+)?}").unwrap(); + + var_regex + .replace_all(s, |caps: ®ex::Captures<'_>| { + if let Some(mat) = caps.get(1) { + if let Ok(value) = env::var(mat.as_str()) { + return scrubbing_regex.replace_all(&value, "").to_string(); + } else if let Some(default_value) = caps.get(2) { + return scrubbing_regex + .replace_all( + default_value + .as_str() + .strip_prefix(":-") + .expect("unable to strip ':-' prefix from default value"), + "", + ) + .to_string(); + } + } + + scrubbing_regex.replace_all(&result, "").to_string() + }) + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde::Deserialize; + use serial_test::serial; + use std::path::PathBuf; + + #[derive(Default, Deserialize, PartialEq, Eq, Debug)] + struct TestConfig { + #[serde(default, deserialize_with = "deserialize_optional_env_var")] + string_var: Option, + #[serde(default, deserialize_with = "deserialize_optional_pathbuf_env_var")] + path_var: Option, + #[serde(default, deserialize_with = "deserialize_optional_usize_env_var")] + usize_var: Option, + } + + #[test] + #[serial] + fn test_deserialize_optional_env_var_is_present() { + unsafe { env::set_var("TEST_VAR_DESERIALIZE_OPTION", "localhost") }; + let yaml_data = indoc!( + r#" + string_var: ${TEST_VAR_DESERIALIZE_OPTION} + path_var: /some/path + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.string_var, Some("localhost".to_string())); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + assert_eq!(config.usize_var, Some(123)); + unsafe { env::remove_var("TEST_VAR_DESERIALIZE_OPTION") }; + } + + #[test] + fn test_deserialize_optional_env_var_empty_env_var_uses_default_value_if_provided() { + let yaml_data = indoc!( + r#" + string_var: ${TEST_VAR_DESERIALIZE_OPTION_UNDEFINED:-localhost} + path_var: /some/path + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.string_var, Some("localhost".to_string())); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + assert_eq!(config.usize_var, Some(123)); + } + + #[test] + #[serial] + fn test_deserialize_optional_env_var_does_not_overwrite_non_env_value() { + unsafe { env::set_var("TEST_VAR_DESERIALIZE_OPTION_NO_OVERWRITE", "localhost") }; + let yaml_data = indoc!( + r#" + string_var: www.example.com + path_var: /some/path + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.string_var, Some("www.example.com".to_string())); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + assert_eq!(config.usize_var, Some(123)); + unsafe { env::remove_var("TEST_VAR_DESERIALIZE_OPTION_NO_OVERWRITE") }; + } + + #[test] + fn test_deserialize_optional_env_var_empty() { + let yaml_data = indoc!( + r#" + path_var: /some/path + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.string_var, None); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + assert_eq!(config.usize_var, Some(123)); + } + + #[test] + #[serial] + fn test_deserialize_optional_pathbuf_env_var_is_present() { + unsafe { env::set_var("TEST_VAR_DESERIALIZE_OPTION_PATHBUF", "/some/path") }; + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: ${TEST_VAR_DESERIALIZE_OPTION_PATHBUF} + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.usize_var, Some(123)); + unsafe { env::remove_var("TEST_VAR_DESERIALIZE_OPTION_PATHBUF") }; + } + + #[test] + fn test_deserialize_optional_pathbuf_env_var_empty_env_var_uses_default_value_if_provided() { + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: ${TEST_VAR_DESERIALIZE_OPTION_PATHBUF_UNDEFINED:-/some/path} + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.usize_var, Some(123)); + } + + #[test] + #[serial] + fn test_deserialize_optional_pathbuf_env_var_does_not_overwrite_non_env_value() { + unsafe { + env::set_var( + "TEST_VAR_DESERIALIZE_OPTION_PATHBUF_NO_OVERWRITE", + "/something/else", + ) + }; + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: /some/path + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.usize_var, Some(123)); + unsafe { env::remove_var("TEST_VAR_DESERIALIZE_OPTION_PATHBUF_NO_OVERWRITE") }; + } + + #[test] + fn test_deserialize_optional_pathbuf_env_var_empty() { + let yaml_data = indoc!( + r#" + string_var: hithere + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.path_var, None); + assert_eq!(config.usize_var, Some(123)); + } + + #[test] + #[serial] + fn test_deserialize_optional_usize_env_var_is_present() { + unsafe { env::set_var("TEST_VAR_DESERIALIZE_OPTION_USIZE", "123") }; + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: /some/path + usize_var: ${TEST_VAR_DESERIALIZE_OPTION_USIZE} + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.usize_var, Some(123)); + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + unsafe { env::remove_var("TEST_VAR_DESERIALIZE_OPTION_USIZE") }; + } + + #[test] + fn test_deserialize_optional_usize_env_var_uses_default_value_if_provided() { + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: /some/path + usize_var: ${TEST_VAR_DESERIALIZE_OPTION_USIZE_UNDEFINED:-123} + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.usize_var, Some(123)); + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + } + + #[test] + #[serial] + fn test_deserialize_optional_usize_env_var_does_not_overwrite_non_env_value() { + unsafe { env::set_var("TEST_VAR_DESERIALIZE_OPTION_NO_OVERWRITE_USIZE", "456") }; + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: /some/path + usize_var: 123 + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.usize_var, Some(123)); + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + unsafe { env::remove_var("TEST_VAR_DESERIALIZE_OPTION_NO_OVERWRITE_USIZE") }; + } + + #[test] + fn test_deserialize_optional_usize_env_var_invalid_number() { + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: /some/path + usize_var: "holo" + "# + ); + let result: Result = serde_yaml::from_str(yaml_data); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("invalid digit found in string")); + } + + #[test] + fn test_deserialize_optional_usize_env_var_empty() { + let yaml_data = indoc!( + r#" + string_var: hithere + path_var: /some/path + "# + ); + + let config: TestConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_eq!(config.usize_var, None); + assert_eq!(config.string_var, Some("hithere".to_string())); + assert_eq!(config.path_var, Some(PathBuf::from("/some/path"))); + } + + #[test] + fn test_interpolate_env_vars_defaults_to_original_string_if_not_in_yaml_interpolation_format() { + let var = interpolate_env_vars("TEST_VAR_INTERPOLATION_NON_YAML"); + + assert_str_eq!(var, "TEST_VAR_INTERPOLATION_NON_YAML"); + } + + #[test] + #[serial] + fn test_interpolate_env_vars_scrubs_all_unnecessary_characters() { + unsafe { + env::set_var( + "TEST_VAR_INTERPOLATION_UNNECESSARY_CHARACTERS", + r#""" + `"'https://dontdo:this@testing.com/query?test=%20query#results'"` {([\|])} + """#, + ) + }; + + let var = interpolate_env_vars("${TEST_VAR_INTERPOLATION_UNNECESSARY_CHARACTERS}"); + + assert_str_eq!( + var, + "https://dontdo:this@testing.com/query?test=%20query#results" + ); + unsafe { env::remove_var("TEST_VAR_INTERPOLATION_UNNECESSARY_CHARACTERS") }; + } + + #[test] + #[serial] + fn test_interpolate_env_vars_scrubs_all_unnecessary_characters_for_default_values() { + let var = interpolate_env_vars( + r#"${UNSET:-`"'https://dontdo:this@testing.com/query?test=%20query#results'"` {([\|])}}"#, + ); + + assert_str_eq!( + var, + "https://dontdo:this@testing.com/query?test=%20query#results" + ); + } + + #[test] + fn test_interpolate_env_vars_scrubs_all_unnecessary_characters_from_non_environment_variable() { + let var = + interpolate_env_vars("https://dontdo:this@testing.com/query?test=%20query#results"); + + assert_str_eq!( + var, + "https://dontdo:this@testing.com/query?test=%20query#results" + ); + } +} diff --git a/src/providers/aws_secrets_manager.rs b/src/providers/aws_secrets_manager.rs index 588d333..e4d4ec8 100644 --- a/src/providers/aws_secrets_manager.rs +++ b/src/providers/aws_secrets_manager.rs @@ -1,3 +1,4 @@ +use crate::config::deserialize_optional_env_var; use crate::providers::SecretProvider; use anyhow::Context; use anyhow::Result; @@ -32,8 +33,10 @@ use validator::Validate; #[serde(deny_unknown_fields)] pub struct AwsSecretsManagerProvider { #[validate(required)] + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub aws_profile: Option, #[validate(required)] + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub aws_region: Option, } diff --git a/src/providers/azure_key_vault.rs b/src/providers/azure_key_vault.rs index 07e60fa..d1a78e2 100644 --- a/src/providers/azure_key_vault.rs +++ b/src/providers/azure_key_vault.rs @@ -1,3 +1,4 @@ +use crate::config::deserialize_optional_env_var; use crate::providers::SecretProvider; use anyhow::{Context, Result}; use azure_identity::DefaultAzureCredential; @@ -30,6 +31,7 @@ use validator::Validate; #[serde(deny_unknown_fields)] pub struct AzureKeyVaultProvider { #[validate(required)] + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub vault_name: Option, } diff --git a/src/providers/gcp_secret_manager.rs b/src/providers/gcp_secret_manager.rs index df3cec6..4cc3ec5 100644 --- a/src/providers/gcp_secret_manager.rs +++ b/src/providers/gcp_secret_manager.rs @@ -1,3 +1,4 @@ +use crate::config::deserialize_optional_env_var; use crate::providers::SecretProvider; use anyhow::{Context, Result, anyhow}; use gcloud_sdk::google::cloud::secretmanager::v1; @@ -39,6 +40,7 @@ type SecretsManagerClient = GoogleApi, } diff --git a/src/providers/gopass.rs b/src/providers/gopass.rs index a0de4e1..3c21971 100644 --- a/src/providers/gopass.rs +++ b/src/providers/gopass.rs @@ -1,3 +1,4 @@ +use crate::config::deserialize_optional_env_var; use crate::providers::{ENV_PATH, SecretProvider}; use anyhow::{Context, Result, anyhow}; use serde::{Deserialize, Serialize}; @@ -28,6 +29,7 @@ use validator::Validate; #[derive(Debug, Default, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct GopassProvider { + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub store: Option, } diff --git a/src/providers/local.rs b/src/providers/local.rs index 8e53e6d..0040d9a 100644 --- a/src/providers/local.rs +++ b/src/providers/local.rs @@ -1,3 +1,5 @@ +use crate::config::deserialize_optional_env_var; +use crate::config::deserialize_optional_pathbuf_env_var; use anyhow::{Context, anyhow, bail}; use secrecy::{ExposeSecret, SecretString}; use std::collections::HashMap; @@ -50,14 +52,21 @@ use validator::Validate; #[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct LocalProvider { + #[serde(default, deserialize_with = "deserialize_optional_pathbuf_env_var")] pub password_file: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub git_branch: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub git_remote_url: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub git_user_name: Option, #[validate(email)] + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub git_user_email: Option, + #[serde(default, deserialize_with = "deserialize_optional_pathbuf_env_var")] pub git_executable: Option, #[serde(skip)] + #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub runtime_provider_name: Option, }