refactor: Refactor configuration structs directly into the provider definition to simplify validation, structs, and future extensions
This commit is contained in:
Generated
+1297
-48
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gman"
|
name = "gman"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||||
description = "Universal secret management and injection tool"
|
description = "Universal secret management and injection tool"
|
||||||
@@ -49,6 +49,9 @@ indoc = "2.0.6"
|
|||||||
regex = "1.11.2"
|
regex = "1.11.2"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
tempfile = "3.22.0"
|
tempfile = "3.22.0"
|
||||||
|
aws-sdk-secretsmanager = "1.88.0"
|
||||||
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
|
aws-config = { version = "1.8.6", features = ["behavior-version-latest"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
|
|||||||
+14
-15
@@ -1,6 +1,6 @@
|
|||||||
use crate::command::preview_command;
|
use crate::command::preview_command;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use gman::config::{Config, ProviderConfig, RunConfig};
|
use gman::config::{Config, RunConfig};
|
||||||
use gman::providers::SecretProvider;
|
use gman::providers::SecretProvider;
|
||||||
use heck::ToSnakeCase;
|
use heck::ToSnakeCase;
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
@@ -15,9 +15,8 @@ const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}";
|
|||||||
const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}";
|
const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}";
|
||||||
|
|
||||||
pub fn wrap_and_run_command(
|
pub fn wrap_and_run_command(
|
||||||
secrets_provider: Box<dyn SecretProvider>,
|
secrets_provider: &mut dyn SecretProvider,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
provider_config: &ProviderConfig,
|
|
||||||
tokens: Vec<OsString>,
|
tokens: Vec<OsString>,
|
||||||
profile_name: Option<String>,
|
profile_name: Option<String>,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
@@ -51,7 +50,7 @@ pub fn wrap_and_run_command(
|
|||||||
run_config_profile_name
|
run_config_profile_name
|
||||||
);
|
);
|
||||||
secrets_provider
|
secrets_provider
|
||||||
.get_secret(provider_config, key.to_snake_case().to_uppercase().as_str())
|
.get_secret(key.to_snake_case().to_uppercase().as_str())
|
||||||
.ok()
|
.ok()
|
||||||
.map_or_else(
|
.map_or_else(
|
||||||
|| {
|
|| {
|
||||||
@@ -254,7 +253,7 @@ pub fn parse_args(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::cli::generate_files_secret_injections;
|
use crate::cli::generate_files_secret_injections;
|
||||||
use gman::config::{Config, ProviderConfig, RunConfig};
|
use gman::config::{Config, RunConfig};
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
@@ -264,16 +263,16 @@ mod tests {
|
|||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"Dummy"
|
"Dummy"
|
||||||
}
|
}
|
||||||
fn get_secret(&self, _config: &ProviderConfig, key: &str) -> Result<String> {
|
fn get_secret(&self, key: &str) -> Result<String> {
|
||||||
Ok(format!("{}_VAL", key))
|
Ok(format!("{}_VAL", key))
|
||||||
}
|
}
|
||||||
fn set_secret(&self, _config: &ProviderConfig, _key: &str, _value: &str) -> Result<()> {
|
fn set_secret(&self, _key: &str, _value: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn delete_secret(&self, _config: &ProviderConfig, _key: &str) -> Result<()> {
|
fn delete_secret(&self, _key: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn sync(&self, _config: &mut ProviderConfig) -> Result<()> {
|
fn sync(&mut self) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,10 +344,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_wrap_and_run_command_no_profile() {
|
fn test_wrap_and_run_command_no_profile() {
|
||||||
let cfg = Config::default();
|
let cfg = Config::default();
|
||||||
let provider_cfg = ProviderConfig::default();
|
let mut dummy = DummyProvider;
|
||||||
let prov: Box<dyn SecretProvider> = Box::new(DummyProvider);
|
let prov: &mut dyn SecretProvider = &mut dummy;
|
||||||
let tokens = vec![OsString::from("echo"), OsString::from("hi")];
|
let tokens = vec![OsString::from("echo"), OsString::from("hi")];
|
||||||
let err = wrap_and_run_command(prov, &cfg, &provider_cfg, tokens, None, true).unwrap_err();
|
let err = wrap_and_run_command(prov, &cfg, tokens, None, true).unwrap_err();
|
||||||
assert!(err.to_string().contains("No run profile found"));
|
assert!(err.to_string().contains("No run profile found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,13 +366,13 @@ mod tests {
|
|||||||
run_configs: Some(vec![run_cfg]),
|
run_configs: Some(vec![run_cfg]),
|
||||||
..Config::default()
|
..Config::default()
|
||||||
};
|
};
|
||||||
let provider_cfg = ProviderConfig::default();
|
let mut dummy = DummyProvider;
|
||||||
let prov: Box<dyn SecretProvider> = Box::new(DummyProvider);
|
let prov: &mut dyn SecretProvider = &mut dummy;
|
||||||
|
|
||||||
// Capture stderr for dry_run preview
|
// Capture stderr for dry_run preview
|
||||||
let tokens = vec![OsString::from("echo"), OsString::from("hello")];
|
let tokens = vec![OsString::from("echo"), OsString::from("hello")];
|
||||||
// Best-effort: ensure function does not error under dry_run
|
// Best-effort: ensure function does not error under dry_run
|
||||||
let res = wrap_and_run_command(prov, &cfg, &provider_cfg, tokens, None, true);
|
let res = wrap_and_run_command(prov, &cfg, tokens, None, true);
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
// Not asserting output text to keep test platform-agnostic
|
// Not asserting output text to keep test platform-agnostic
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-21
@@ -116,7 +116,8 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
if let Err(e) = log4rs::init_config(utils::init_logging_config()) {
|
if let Err(e) = log4rs::init_config(utils::init_logging_config()) {
|
||||||
eprintln!("Failed to initialize logging: {e}");
|
eprintln!("Failed to initialize logging: {e}");
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ fn main() -> Result<()> {
|
|||||||
read_all_stdin().with_context(|| "unable to read plaintext from stdin")?;
|
read_all_stdin().with_context(|| "unable to read plaintext from stdin")?;
|
||||||
let snake_case_name = name.to_snake_case().to_uppercase();
|
let snake_case_name = name.to_snake_case().to_uppercase();
|
||||||
secrets_provider
|
secrets_provider
|
||||||
.set_secret(&provider_config, &snake_case_name, plaintext.trim_end())
|
.set_secret(&snake_case_name, plaintext.trim_end())
|
||||||
.map(|_| match cli.output {
|
.map(|_| match cli.output {
|
||||||
Some(_) => (),
|
Some(_) => (),
|
||||||
None => println!("✓ Secret '{snake_case_name}' added to the vault."),
|
None => println!("✓ Secret '{snake_case_name}' added to the vault."),
|
||||||
@@ -153,7 +154,7 @@ fn main() -> Result<()> {
|
|||||||
Commands::Get { name } => {
|
Commands::Get { name } => {
|
||||||
let snake_case_name = name.to_snake_case().to_uppercase();
|
let snake_case_name = name.to_snake_case().to_uppercase();
|
||||||
secrets_provider
|
secrets_provider
|
||||||
.get_secret(&provider_config, &snake_case_name)
|
.get_secret(&snake_case_name)
|
||||||
.map(|secret| match cli.output {
|
.map(|secret| match cli.output {
|
||||||
Some(OutputFormat::Json) => {
|
Some(OutputFormat::Json) => {
|
||||||
let json_output = serde_json::json!({
|
let json_output = serde_json::json!({
|
||||||
@@ -175,7 +176,7 @@ fn main() -> Result<()> {
|
|||||||
read_all_stdin().with_context(|| "unable to read plaintext from stdin")?;
|
read_all_stdin().with_context(|| "unable to read plaintext from stdin")?;
|
||||||
let snake_case_name = name.to_snake_case().to_uppercase();
|
let snake_case_name = name.to_snake_case().to_uppercase();
|
||||||
secrets_provider
|
secrets_provider
|
||||||
.update_secret(&provider_config, &snake_case_name, plaintext.trim_end())
|
.update_secret(&snake_case_name, plaintext.trim_end())
|
||||||
.map(|_| match cli.output {
|
.map(|_| match cli.output {
|
||||||
Some(_) => (),
|
Some(_) => (),
|
||||||
None => println!("✓ Secret '{snake_case_name}' updated in the vault."),
|
None => println!("✓ Secret '{snake_case_name}' updated in the vault."),
|
||||||
@@ -183,16 +184,14 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Commands::Delete { name } => {
|
Commands::Delete { name } => {
|
||||||
let snake_case_name = name.to_snake_case().to_uppercase();
|
let snake_case_name = name.to_snake_case().to_uppercase();
|
||||||
secrets_provider
|
secrets_provider.delete_secret(&snake_case_name).map(|_| {
|
||||||
.delete_secret(&provider_config, &snake_case_name)
|
if cli.output.is_none() {
|
||||||
.map(|_| {
|
println!("✓ Secret '{snake_case_name}' deleted from the vault.")
|
||||||
if cli.output.is_none() {
|
}
|
||||||
println!("✓ Secret '{snake_case_name}' deleted from the vault.")
|
})?;
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Commands::List {} => {
|
Commands::List {} => {
|
||||||
let secrets = secrets_provider.list_secrets(&provider_config)?;
|
let secrets = secrets_provider.list_secrets()?;
|
||||||
if secrets.is_empty() {
|
if secrets.is_empty() {
|
||||||
match cli.output {
|
match cli.output {
|
||||||
Some(OutputFormat::Json) => {
|
Some(OutputFormat::Json) => {
|
||||||
@@ -218,21 +217,14 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Sync {} => {
|
Commands::Sync {} => {
|
||||||
secrets_provider.sync(&mut provider_config).map(|_| {
|
secrets_provider.sync().map(|_| {
|
||||||
if cli.output.is_none() {
|
if cli.output.is_none() {
|
||||||
println!("✓ Secrets synchronized with remote")
|
println!("✓ Secrets synchronized with remote")
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Commands::External(tokens) => {
|
Commands::External(tokens) => {
|
||||||
wrap_and_run_command(
|
wrap_and_run_command(secrets_provider, &config, tokens, cli.profile, cli.dry_run)?;
|
||||||
secrets_provider,
|
|
||||||
&config,
|
|
||||||
&provider_config,
|
|
||||||
tokens,
|
|
||||||
cli.profile,
|
|
||||||
cli.dry_run,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
Commands::Completions { shell } => {
|
Commands::Completions { shell } => {
|
||||||
let mut cmd = Cli::command();
|
let mut cmd = Cli::command();
|
||||||
|
|||||||
+23
-31
@@ -25,7 +25,7 @@ use anyhow::{Context, Result};
|
|||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::{DisplayFromStr, skip_serializing_none};
|
use serde_with::skip_serializing_none;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
@@ -97,8 +97,6 @@ fn flags_or_files(run_config: &RunConfig) -> Result<(), ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[skip_serializing_none]
|
|
||||||
/// Configuration for a secret provider.
|
/// Configuration for a secret provider.
|
||||||
///
|
///
|
||||||
/// Example: create a local provider config and validate it
|
/// Example: create a local provider config and validate it
|
||||||
@@ -108,37 +106,27 @@ fn flags_or_files(run_config: &RunConfig) -> Result<(), ValidationError> {
|
|||||||
/// use gman::providers::local::LocalProvider;
|
/// use gman::providers::local::LocalProvider;
|
||||||
/// use validator::Validate;
|
/// use validator::Validate;
|
||||||
///
|
///
|
||||||
/// let provider_type = SupportedProvider::Local(LocalProvider);
|
/// let provider_type = SupportedProvider::Local { provider_def: LocalProvider::default() };
|
||||||
/// let provider_config = ProviderConfig { provider_type, ..Default::default() };
|
/// let provider_config = ProviderConfig { provider_type, ..Default::default() };
|
||||||
/// provider_config.validate().unwrap();
|
/// provider_config.validate().unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[skip_serializing_none]
|
||||||
pub struct ProviderConfig {
|
pub struct ProviderConfig {
|
||||||
#[validate(required)]
|
#[validate(required)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde(flatten, rename = "type")]
|
||||||
#[serde(rename = "type")]
|
#[validate(nested)]
|
||||||
pub provider_type: SupportedProvider,
|
pub provider_type: SupportedProvider,
|
||||||
pub password_file: Option<PathBuf>,
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
pub git_remote_url: Option<String>,
|
|
||||||
pub git_user_name: Option<String>,
|
|
||||||
#[validate(email)]
|
|
||||||
pub git_user_email: Option<String>,
|
|
||||||
pub git_executable: Option<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProviderConfig {
|
impl Default for ProviderConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Some("local".into()),
|
name: Some("local".into()),
|
||||||
provider_type: SupportedProvider::Local(LocalProvider),
|
provider_type: SupportedProvider::Local {
|
||||||
password_file: Config::local_provider_password_file(),
|
provider_def: LocalProvider::default(),
|
||||||
git_branch: Some("main".into()),
|
},
|
||||||
git_remote_url: None,
|
|
||||||
git_user_name: None,
|
|
||||||
git_user_email: None,
|
|
||||||
git_executable: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,11 +139,11 @@ impl ProviderConfig {
|
|||||||
/// let provider_config = ProviderConfig::default().extract_provider();
|
/// let provider_config = ProviderConfig::default().extract_provider();
|
||||||
/// println!("using provider: {}", provider_config.name());
|
/// println!("using provider: {}", provider_config.name());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn extract_provider(&self) -> Box<dyn SecretProvider> {
|
pub fn extract_provider(&mut self) -> &mut dyn SecretProvider {
|
||||||
match &self.provider_type {
|
match &mut self.provider_type {
|
||||||
SupportedProvider::Local(p) => {
|
SupportedProvider::Local { provider_def } => {
|
||||||
debug!("Using local secret provider");
|
debug!("Using local secret provider");
|
||||||
Box::new(*p)
|
provider_def
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +161,7 @@ impl ProviderConfig {
|
|||||||
/// use gman::providers::local::LocalProvider;
|
/// use gman::providers::local::LocalProvider;
|
||||||
/// use validator::Validate;
|
/// use validator::Validate;
|
||||||
///
|
///
|
||||||
/// let provider_type = SupportedProvider::Local(LocalProvider);
|
/// let provider_type = SupportedProvider::Local{ provider_def: LocalProvider::default() };
|
||||||
/// let provider_config = ProviderConfig { provider_type, ..Default::default() };
|
/// let provider_config = ProviderConfig { provider_type, ..Default::default() };
|
||||||
/// let cfg = Config{ providers: vec![provider_config], ..Default::default() };
|
/// let cfg = Config{ providers: vec![provider_config], ..Default::default() };
|
||||||
/// cfg.validate().unwrap();
|
/// cfg.validate().unwrap();
|
||||||
@@ -289,12 +277,16 @@ pub fn load_config() -> Result<Config> {
|
|||||||
config
|
config
|
||||||
.providers
|
.providers
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.filter(|p| matches!(p.provider_type, SupportedProvider::Local(_)))
|
.filter(|p| matches!(p.provider_type, SupportedProvider::Local { .. }))
|
||||||
.for_each(|p| {
|
.for_each(|p| match p.provider_type {
|
||||||
if p.password_file.is_none()
|
SupportedProvider::Local {
|
||||||
&& let Some(local_password_file) = Config::local_provider_password_file()
|
ref mut provider_def,
|
||||||
{
|
} => {
|
||||||
p.password_file = Some(local_password_file);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+97
-88
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::config::ProviderConfig;
|
use crate::config::Config;
|
||||||
use crate::providers::SecretProvider;
|
use crate::providers::SecretProvider;
|
||||||
use crate::providers::git_sync::{SyncOpts, repo_name_from_url, sync_and_push};
|
use crate::providers::git_sync::{SyncOpts, repo_name_from_url, sync_and_push};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -21,27 +21,12 @@ use chacha20poly1305::{
|
|||||||
};
|
};
|
||||||
use dialoguer::{Input, theme};
|
use dialoguer::{Input, theme};
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
use theme::ColorfulTheme;
|
use theme::ColorfulTheme;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
/// Configuration for the local file-based provider.
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LocalProviderConfig {
|
|
||||||
pub vault_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LocalProviderConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
vault_path: dirs::home_dir()
|
|
||||||
.map(|p| p.join(".gman_vault"))
|
|
||||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
|
||||||
.unwrap_or_else(|| ".gman_vault".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// File-based vault provider with optional Git sync.
|
/// File-based vault provider with optional Git sync.
|
||||||
///
|
///
|
||||||
/// This provider stores encrypted envelopes in a per-user configuration
|
/// This provider stores encrypted envelopes in a per-user configuration
|
||||||
@@ -54,37 +39,59 @@ impl Default for LocalProviderConfig {
|
|||||||
/// use gman::providers::SecretProvider;
|
/// use gman::providers::SecretProvider;
|
||||||
/// use gman::config::Config;
|
/// use gman::config::Config;
|
||||||
///
|
///
|
||||||
/// let provider = LocalProvider;
|
/// let provider = LocalProvider::default();
|
||||||
/// let cfg = Config::default();
|
/// let cfg = Config::default();
|
||||||
/// // Will prompt for a password when reading/writing secrets unless a
|
/// // Will prompt for a password when reading/writing secrets unless a
|
||||||
/// // password file is configured.
|
/// // password file is configured.
|
||||||
/// // provider.set_secret(&cfg, "MY_SECRET", "value")?;
|
/// // provider.set_secret(&cfg, "MY_SECRET", "value")?;
|
||||||
/// # Ok::<(), anyhow::Error>(())
|
/// # Ok::<(), anyhow::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct LocalProvider;
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct LocalProvider {
|
||||||
|
pub password_file: Option<PathBuf>,
|
||||||
|
pub git_branch: Option<String>,
|
||||||
|
pub git_remote_url: Option<String>,
|
||||||
|
pub git_user_name: Option<String>,
|
||||||
|
#[validate(email)]
|
||||||
|
pub git_user_email: Option<String>,
|
||||||
|
pub git_executable: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LocalProvider {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
password_file: Config::local_provider_password_file(),
|
||||||
|
git_branch: Some("main".into()),
|
||||||
|
git_remote_url: None,
|
||||||
|
git_user_name: None,
|
||||||
|
git_user_email: None,
|
||||||
|
git_executable: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SecretProvider for LocalProvider {
|
impl SecretProvider for LocalProvider {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"LocalProvider"
|
"LocalProvider"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_secret(&self, config: &ProviderConfig, key: &str) -> Result<String> {
|
fn get_secret(&self, key: &str) -> Result<String> {
|
||||||
let vault_path = active_vault_path(config)?;
|
let vault_path = self.active_vault_path()?;
|
||||||
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||||
let envelope = vault
|
let envelope = vault
|
||||||
.get(key)
|
.get(key)
|
||||||
.with_context(|| format!("key '{key}' not found in the vault"))?;
|
.with_context(|| format!("key '{key}' not found in the vault"))?;
|
||||||
|
|
||||||
let password = get_password(config)?;
|
let password = self.get_password()?;
|
||||||
let plaintext = decrypt_string(&password, envelope)?;
|
let plaintext = decrypt_string(&password, envelope)?;
|
||||||
drop(password);
|
drop(password);
|
||||||
|
|
||||||
Ok(plaintext)
|
Ok(plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_secret(&self, config: &ProviderConfig, key: &str, value: &str) -> Result<()> {
|
fn set_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||||
let vault_path = active_vault_path(config)?;
|
let vault_path = self.active_vault_path()?;
|
||||||
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||||
if vault.contains_key(key) {
|
if vault.contains_key(key) {
|
||||||
error!(
|
error!(
|
||||||
@@ -93,7 +100,7 @@ impl SecretProvider for LocalProvider {
|
|||||||
bail!("key '{key}' already exists");
|
bail!("key '{key}' already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
let password = get_password(config)?;
|
let password = self.get_password()?;
|
||||||
let envelope = encrypt_string(&password, value)?;
|
let envelope = encrypt_string(&password, value)?;
|
||||||
drop(password);
|
drop(password);
|
||||||
|
|
||||||
@@ -102,11 +109,11 @@ impl SecretProvider for LocalProvider {
|
|||||||
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_secret(&self, config: &ProviderConfig, key: &str, value: &str) -> Result<()> {
|
fn update_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||||
let vault_path = active_vault_path(config)?;
|
let vault_path = self.active_vault_path()?;
|
||||||
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||||
|
|
||||||
let password = get_password(config)?;
|
let password = self.get_password()?;
|
||||||
let envelope = encrypt_string(&password, value)?;
|
let envelope = encrypt_string(&password, value)?;
|
||||||
drop(password);
|
drop(password);
|
||||||
|
|
||||||
@@ -125,8 +132,8 @@ impl SecretProvider for LocalProvider {
|
|||||||
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_secret(&self, config: &ProviderConfig, key: &str) -> Result<()> {
|
fn delete_secret(&self, key: &str) -> Result<()> {
|
||||||
let vault_path = active_vault_path(config)?;
|
let vault_path = self.active_vault_path()?;
|
||||||
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||||
if !vault.contains_key(key) {
|
if !vault.contains_key(key) {
|
||||||
error!("Key '{key}' does not exist in the vault.");
|
error!("Key '{key}' does not exist in the vault.");
|
||||||
@@ -137,18 +144,18 @@ impl SecretProvider for LocalProvider {
|
|||||||
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_secrets(&self, config: &ProviderConfig) -> Result<Vec<String>> {
|
fn list_secrets(&self) -> Result<Vec<String>> {
|
||||||
let vault_path = active_vault_path(config)?;
|
let vault_path = self.active_vault_path()?;
|
||||||
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||||
let keys: Vec<String> = vault.keys().cloned().collect();
|
let keys: Vec<String> = vault.keys().cloned().collect();
|
||||||
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync(&self, config: &mut ProviderConfig) -> Result<()> {
|
fn sync(&mut self) -> Result<()> {
|
||||||
let mut config_changed = false;
|
let mut config_changed = false;
|
||||||
|
|
||||||
if config.git_branch.is_none() {
|
if self.git_branch.is_none() {
|
||||||
config_changed = true;
|
config_changed = true;
|
||||||
debug!("Prompting user to set git_branch in config for sync");
|
debug!("Prompting user to set git_branch in config for sync");
|
||||||
let branch: String = Input::with_theme(&ColorfulTheme::default())
|
let branch: String = Input::with_theme(&ColorfulTheme::default())
|
||||||
@@ -156,18 +163,18 @@ impl SecretProvider for LocalProvider {
|
|||||||
.default("main".into())
|
.default("main".into())
|
||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
|
|
||||||
config.git_branch = Some(branch);
|
self.git_branch = Some(branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.git_remote_url.is_none() {
|
if self.git_remote_url.is_none() {
|
||||||
config_changed = true;
|
config_changed = true;
|
||||||
debug!("Prompting user to set git_remote in config for sync");
|
debug!("Prompting user to set git_remote in config for sync");
|
||||||
let remote: String = Input::with_theme(&ColorfulTheme::default())
|
let remote: String = Input::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt("Enter remote git URL to sync with")
|
.with_prompt("Enter remote git URL to sync with")
|
||||||
.validate_with(|s: &String| {
|
.validate_with(|s: &String| {
|
||||||
ProviderConfig {
|
LocalProvider {
|
||||||
git_remote_url: Some(s.clone()),
|
git_remote_url: Some(s.clone()),
|
||||||
..ProviderConfig::default()
|
..LocalProvider::default()
|
||||||
}
|
}
|
||||||
.validate()
|
.validate()
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -175,27 +182,66 @@ impl SecretProvider for LocalProvider {
|
|||||||
})
|
})
|
||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
|
|
||||||
config.git_remote_url = Some(remote);
|
self.git_remote_url = Some(remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
if config_changed {
|
if config_changed {
|
||||||
debug!("Saving updated config");
|
debug!("Saving updated config");
|
||||||
confy::store("gman", "config", &config)
|
confy::store("gman", "config", &self)
|
||||||
.with_context(|| "failed to save updated config")?;
|
.with_context(|| "failed to save updated config")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sync_opts = SyncOpts {
|
let sync_opts = SyncOpts {
|
||||||
remote_url: &config.git_remote_url,
|
remote_url: &self.git_remote_url,
|
||||||
branch: &config.git_branch,
|
branch: &self.git_branch,
|
||||||
user_name: &config.git_user_name,
|
user_name: &self.git_user_name,
|
||||||
user_email: &config.git_user_email,
|
user_email: &self.git_user_email,
|
||||||
git_executable: &config.git_executable,
|
git_executable: &self.git_executable,
|
||||||
};
|
};
|
||||||
|
|
||||||
sync_and_push(&sync_opts)
|
sync_and_push(&sync_opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LocalProvider {
|
||||||
|
fn repo_dir_for_config(&self) -> Result<Option<PathBuf>> {
|
||||||
|
if let Some(remote) = &self.git_remote_url {
|
||||||
|
let name = repo_name_from_url(remote);
|
||||||
|
let dir = base_config_dir()?.join(format!(".{}", name));
|
||||||
|
Ok(Some(dir))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_vault_path(&self) -> Result<PathBuf> {
|
||||||
|
if let Some(dir) = self.repo_dir_for_config()?
|
||||||
|
&& dir.exists()
|
||||||
|
{
|
||||||
|
return Ok(dir.join("vault.yml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
default_vault_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_password(&self) -> Result<SecretString> {
|
||||||
|
if let Some(password_file) = &self.password_file {
|
||||||
|
let password = SecretString::new(
|
||||||
|
fs::read_to_string(password_file)
|
||||||
|
.with_context(|| format!("failed to read password file {:?}", password_file))?
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(password)
|
||||||
|
} else {
|
||||||
|
let password = rpassword::prompt_password("\nPassword: ")?;
|
||||||
|
Ok(SecretString::new(password.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn default_vault_path() -> Result<PathBuf> {
|
fn default_vault_path() -> Result<PathBuf> {
|
||||||
let xdg_path = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from);
|
let xdg_path = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from);
|
||||||
|
|
||||||
@@ -213,26 +259,6 @@ fn base_config_dir() -> Result<PathBuf> {
|
|||||||
.ok_or_else(|| anyhow!("Failed to determine config dir"))
|
.ok_or_else(|| anyhow!("Failed to determine config dir"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repo_dir_for_config(config: &ProviderConfig) -> Result<Option<PathBuf>> {
|
|
||||||
if let Some(remote) = &config.git_remote_url {
|
|
||||||
let name = repo_name_from_url(remote);
|
|
||||||
let dir = base_config_dir()?.join(format!(".{}", name));
|
|
||||||
Ok(Some(dir))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn active_vault_path(config: &ProviderConfig) -> Result<PathBuf> {
|
|
||||||
if let Some(dir) = repo_dir_for_config(config)?
|
|
||||||
&& dir.exists()
|
|
||||||
{
|
|
||||||
return Ok(dir.join("vault.yml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
default_vault_path()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_vault(path: &Path) -> Result<HashMap<String, String>> {
|
fn load_vault(path: &Path) -> Result<HashMap<String, String>> {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(HashMap::new());
|
return Ok(HashMap::new());
|
||||||
@@ -394,23 +420,6 @@ fn decrypt_string(password: &SecretString, envelope: &str) -> Result<String> {
|
|||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_password(config: &ProviderConfig) -> Result<SecretString> {
|
|
||||||
if let Some(password_file) = &config.password_file {
|
|
||||||
let password = SecretString::new(
|
|
||||||
fs::read_to_string(password_file)
|
|
||||||
.with_context(|| format!("failed to read password file {:?}", password_file))?
|
|
||||||
.trim()
|
|
||||||
.to_string()
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(password)
|
|
||||||
} else {
|
|
||||||
let password = rpassword::prompt_password("\nPassword: ")?;
|
|
||||||
Ok(SecretString::new(password.into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -448,11 +457,11 @@ mod tests {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let file = dir.path().join("pw.txt");
|
let file = dir.path().join("pw.txt");
|
||||||
fs::write(&file, "secretpw\n").unwrap();
|
fs::write(&file, "secretpw\n").unwrap();
|
||||||
let cfg = ProviderConfig {
|
let provider = LocalProvider {
|
||||||
password_file: Some(file),
|
password_file: Some(file),
|
||||||
..ProviderConfig::default()
|
..LocalProvider::default()
|
||||||
};
|
};
|
||||||
let pw = get_password(&cfg).unwrap();
|
let pw = provider.get_password().unwrap();
|
||||||
assert_eq!(pw.expose_secret(), "secretpw");
|
assert_eq!(pw.expose_secret(), "secretpw");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-32
@@ -2,46 +2,36 @@
|
|||||||
//!
|
//!
|
||||||
//! Implementations provide storage/backends for secrets and a common
|
//! Implementations provide storage/backends for secrets and a common
|
||||||
//! interface used by the CLI.
|
//! interface used by the CLI.
|
||||||
//!
|
|
||||||
//! Selecting a provider from a string:
|
|
||||||
//! ```
|
|
||||||
//! use std::str::FromStr;
|
|
||||||
//! use gman::providers::SupportedProvider;
|
|
||||||
//!
|
|
||||||
//! let p = SupportedProvider::from_str("local").unwrap();
|
|
||||||
//! assert_eq!(p.to_string(), "local");
|
|
||||||
//! ```
|
|
||||||
mod git_sync;
|
mod git_sync;
|
||||||
pub mod local;
|
pub mod local;
|
||||||
|
|
||||||
use crate::config::ProviderConfig;
|
|
||||||
use crate::providers::local::LocalProvider;
|
use crate::providers::local::LocalProvider;
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::str::FromStr;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use validator::{Validate, ValidationErrors};
|
||||||
|
|
||||||
/// A secret storage backend capable of CRUD and sync, with optional
|
/// A secret storage backend capable of CRUD and sync, with optional
|
||||||
/// update and listing
|
/// update and listing
|
||||||
pub trait SecretProvider {
|
pub trait SecretProvider {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
fn get_secret(&self, config: &ProviderConfig, key: &str) -> Result<String>;
|
fn get_secret(&self, key: &str) -> Result<String>;
|
||||||
fn set_secret(&self, config: &ProviderConfig, key: &str, value: &str) -> Result<()>;
|
fn set_secret(&self, key: &str, value: &str) -> Result<()>;
|
||||||
fn update_secret(&self, _config: &ProviderConfig, _key: &str, _value: &str) -> Result<()> {
|
fn update_secret(&self, _key: &str, _value: &str) -> Result<()> {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"update secret not supported for provider {}",
|
"update secret not supported for provider {}",
|
||||||
self.name()
|
self.name()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
fn delete_secret(&self, config: &ProviderConfig, key: &str) -> Result<()>;
|
fn delete_secret(&self, key: &str) -> Result<()>;
|
||||||
fn list_secrets(&self, _config: &ProviderConfig) -> Result<Vec<String>> {
|
fn list_secrets(&self) -> Result<Vec<String>> {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"list secrets is not supported for the provider {}",
|
"list secrets is not supported for the provider {}",
|
||||||
self.name()
|
self.name()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
fn sync(&self, config: &mut ProviderConfig) -> Result<()>;
|
fn sync(&mut self) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors when parsing a provider identifier.
|
/// Errors when parsing a provider identifier.
|
||||||
@@ -52,24 +42,28 @@ pub enum ParseProviderError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Registry of built-in providers.
|
/// Registry of built-in providers.
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")]
|
||||||
|
//TODO test that this works with the AWS config
|
||||||
pub enum SupportedProvider {
|
pub enum SupportedProvider {
|
||||||
Local(LocalProvider),
|
Local {
|
||||||
|
#[serde(flatten)]
|
||||||
|
provider_def: LocalProvider,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for SupportedProvider {
|
||||||
|
fn validate(&self) -> Result<(), ValidationErrors> {
|
||||||
|
match self {
|
||||||
|
SupportedProvider::Local { provider_def } => provider_def.validate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SupportedProvider {
|
impl Default for SupportedProvider {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
SupportedProvider::Local(LocalProvider)
|
SupportedProvider::Local {
|
||||||
}
|
provider_def: LocalProvider::default(),
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for SupportedProvider {
|
|
||||||
type Err = ParseProviderError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.trim().to_lowercase().as_str() {
|
|
||||||
"local" => Ok(SupportedProvider::Local(LocalProvider)),
|
|
||||||
_ => Err(ParseProviderError::Unsupported(s.to_string())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +71,7 @@ impl FromStr for SupportedProvider {
|
|||||||
impl Display for SupportedProvider {
|
impl Display for SupportedProvider {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
SupportedProvider::Local(_) => write!(f, "local"),
|
SupportedProvider::Local { .. } => write!(f, "local"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gman::config::{Config, ProviderConfig, RunConfig};
|
use gman::config::{Config, ProviderConfig, RunConfig};
|
||||||
use gman::providers::SupportedProvider;
|
|
||||||
use gman::providers::local::LocalProvider;
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
@@ -161,65 +159,11 @@ mod tests {
|
|||||||
assert!(run_config.validate().is_err());
|
assert!(run_config.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_provider_config_valid() {
|
|
||||||
let config = ProviderConfig {
|
|
||||||
name: Some("local-test".to_string()),
|
|
||||||
provider_type: SupportedProvider::Local(LocalProvider),
|
|
||||||
password_file: None,
|
|
||||||
git_branch: None,
|
|
||||||
git_remote_url: None,
|
|
||||||
git_user_name: None,
|
|
||||||
git_user_email: Some("test@example.com".to_string()),
|
|
||||||
git_executable: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(config.validate().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_provider_config_invalid_email() {
|
|
||||||
let config = ProviderConfig {
|
|
||||||
name: Some("local-test".to_string()),
|
|
||||||
provider_type: SupportedProvider::Local(LocalProvider),
|
|
||||||
password_file: None,
|
|
||||||
git_branch: None,
|
|
||||||
git_remote_url: None,
|
|
||||||
git_user_name: None,
|
|
||||||
git_user_email: Some("test".to_string()),
|
|
||||||
git_executable: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(config.validate().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_provider_config_missing_name() {
|
|
||||||
let config = ProviderConfig {
|
|
||||||
name: None,
|
|
||||||
provider_type: SupportedProvider::Local(LocalProvider),
|
|
||||||
password_file: None,
|
|
||||||
git_branch: None,
|
|
||||||
git_remote_url: None,
|
|
||||||
git_user_name: None,
|
|
||||||
git_user_email: None,
|
|
||||||
git_executable: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(config.validate().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_provider_config_default() {
|
fn test_provider_config_default() {
|
||||||
let config = ProviderConfig::default();
|
let config = ProviderConfig::default();
|
||||||
|
|
||||||
assert_eq!(config.name, Some("local".to_string()));
|
assert_eq!(config.name, Some("local".to_string()));
|
||||||
assert_eq!(config.git_user_email, None);
|
|
||||||
assert_eq!(config.password_file, Config::local_provider_password_file());
|
|
||||||
assert_eq!(config.git_branch, Some("main".into()));
|
|
||||||
assert_eq!(config.git_remote_url, None);
|
|
||||||
assert_eq!(config.git_user_name, None);
|
|
||||||
assert_eq!(config.git_executable, None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,21 +1,56 @@
|
|||||||
use gman::providers::local::LocalProviderConfig;
|
use gman::config::Config;
|
||||||
|
use gman::providers::local::LocalProvider;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use pretty_assertions::assert_str_eq;
|
use pretty_assertions::assert_str_eq;
|
||||||
|
use validator::Validate;
|
||||||
#[test]
|
|
||||||
fn test_local_provider_config_default() {
|
|
||||||
let config = LocalProviderConfig::default();
|
|
||||||
let expected_path = dirs::home_dir()
|
|
||||||
.map(|p| p.join(".gman_vault"))
|
|
||||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
|
||||||
.unwrap_or_else(|| ".gman_vault".into());
|
|
||||||
assert_str_eq!(config.vault_path, expected_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_local_provider_name() {
|
fn test_local_provider_name() {
|
||||||
use gman::providers::SecretProvider;
|
use gman::providers::SecretProvider;
|
||||||
use gman::providers::local::LocalProvider;
|
use gman::providers::local::LocalProvider;
|
||||||
|
|
||||||
let provider = LocalProvider;
|
let provider = LocalProvider::default();
|
||||||
assert_str_eq!(provider.name(), "LocalProvider");
|
assert_str_eq!(provider.name(), "LocalProvider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_provider_valid() {
|
||||||
|
let provider = LocalProvider {
|
||||||
|
password_file: None,
|
||||||
|
git_branch: None,
|
||||||
|
git_remote_url: None,
|
||||||
|
git_user_name: None,
|
||||||
|
git_user_email: Some("test@example.com".to_string()),
|
||||||
|
git_executable: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(provider.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_provider_invalid_email() {
|
||||||
|
let config = LocalProvider {
|
||||||
|
password_file: None,
|
||||||
|
git_branch: None,
|
||||||
|
git_remote_url: None,
|
||||||
|
git_user_name: None,
|
||||||
|
git_user_email: Some("test".to_string()),
|
||||||
|
git_executable: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(config.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_provider_default() {
|
||||||
|
let provider = LocalProvider::default();
|
||||||
|
assert_eq!(
|
||||||
|
provider.password_file,
|
||||||
|
Config::local_provider_password_file()
|
||||||
|
);
|
||||||
|
assert_eq!(provider.git_branch, Some("main".into()));
|
||||||
|
assert_eq!(provider.git_remote_url, None);
|
||||||
|
assert_eq!(provider.git_user_name, None);
|
||||||
|
assert_eq!(provider.git_user_email, None);
|
||||||
|
assert_eq!(provider.git_executable, None);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,49 +1,20 @@
|
|||||||
use gman::providers::local::LocalProvider;
|
use gman::config::ProviderConfig;
|
||||||
use gman::providers::{ParseProviderError, SupportedProvider};
|
use gman::providers::ParseProviderError;
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::assert_eq;
|
||||||
use std::str::FromStr;
|
use validator::Validate;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_supported_provider_from_str() {
|
|
||||||
assert_eq!(
|
|
||||||
SupportedProvider::from_str("local").unwrap(),
|
|
||||||
SupportedProvider::Local(LocalProvider)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
SupportedProvider::from_str(" Local ").unwrap(),
|
|
||||||
SupportedProvider::Local(LocalProvider)
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
|
||||||
SupportedProvider::from_str("invalid"),
|
|
||||||
Err(ParseProviderError::Unsupported(_))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_supported_provider_display() {
|
|
||||||
assert_str_eq!(SupportedProvider::Local(LocalProvider).to_string(), "local");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_supported_provider_from_str_valid() {
|
|
||||||
assert_eq!(
|
|
||||||
SupportedProvider::from_str("local").unwrap(),
|
|
||||||
SupportedProvider::Local(LocalProvider)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
SupportedProvider::from_str("LOCAL").unwrap(),
|
|
||||||
SupportedProvider::Local(LocalProvider)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_supported_provider_from_str_invalid() {
|
|
||||||
let err = SupportedProvider::from_str("invalid").unwrap_err();
|
|
||||||
assert_str_eq!(err.to_string(), "unsupported provider 'invalid'");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_provider_error_display() {
|
fn test_parse_provider_error_display() {
|
||||||
let err = ParseProviderError::Unsupported("test".to_string());
|
let err = ParseProviderError::Unsupported("test".to_string());
|
||||||
assert_eq!(err.to_string(), "unsupported provider 'test'");
|
assert_eq!(err.to_string(), "unsupported provider 'test'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_provider_config_missing_name() {
|
||||||
|
let config = ProviderConfig {
|
||||||
|
name: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(config.validate().is_err());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user