feat: Subcommand to edit the config directly instead of having to find the file
This commit is contained in:
@@ -7,36 +7,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [0.0.1] - 2025-09-10
|
||||
|
||||
## v0.1.0 (2025-09-15)
|
||||
|
||||
### Fix
|
||||
|
||||
- Pass the changelog to the GHA properly using a file
|
||||
- Potential bug in changelog variable generation
|
||||
- Revert back hacky stuff so I can test with act now
|
||||
- Attempting to use pre-generated bindgens for the aws-lc-sys library
|
||||
- Install openSSL differently to make this work
|
||||
- Address edge case for unknown_musl targets
|
||||
- Install LLVM prereqs for release flow
|
||||
- Updated the release flow to install the external bindgen-cli
|
||||
|
||||
## v0.0.1 (2025-09-12)
|
||||
|
||||
### Feat
|
||||
|
||||
- Azure Key Vault support
|
||||
- GCP Secret Manager support
|
||||
- Full AWS SecretsManager support
|
||||
- AWS Secrets Manager support
|
||||
- Added two new flags to output where gman writes logs to and where it expects the config file to live
|
||||
|
||||
### Fix
|
||||
|
||||
- Made the vault file location more fault tolerant
|
||||
- Attempting to maybe be a bit more explicit about config file handling to fix MacOS tests
|
||||
|
||||
### Refactor
|
||||
|
||||
- Refactor configuration structs directly into the provider definition to simplify validation, structs, and future extensions
|
||||
- Made the creation of the log directories a bit more fault tolerant
|
||||
- Renamed the provider field in a config file to type to make things a little easier to understand; also removed husky
|
||||
|
||||
Generated
+1
-1
@@ -1576,7 +1576,7 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "gman"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gman"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "Universal secret management and injection tool"
|
||||
|
||||
+30
-5
@@ -1,19 +1,21 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Subcommand;
|
||||
use clap::{
|
||||
CommandFactory, Parser, ValueEnum, crate_authors, crate_description, crate_name, crate_version,
|
||||
};
|
||||
use std::ffi::OsString;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Subcommand;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
|
||||
use gman::config::{get_config_file_path, load_config};
|
||||
use gman::config::{Config, get_config_file_path, load_config};
|
||||
use std::ffi::OsString;
|
||||
use std::io::{self, IsTerminal, Read, Write};
|
||||
use std::panic::PanicHookInfo;
|
||||
|
||||
use crate::cli::wrap_and_run_command;
|
||||
use dialoguer::Editor;
|
||||
use std::panic;
|
||||
use std::process::exit;
|
||||
use validator::Validate;
|
||||
use crate::utils::persist_config_file;
|
||||
|
||||
mod cli;
|
||||
mod command;
|
||||
@@ -103,6 +105,9 @@ enum Commands {
|
||||
/// Sync secrets with remote storage (if supported by the provider)
|
||||
Sync {},
|
||||
|
||||
/// Open and edit the config file in the default text editor
|
||||
Config {},
|
||||
|
||||
/// Wrap the provided command and supply it with secrets as environment variables or as
|
||||
/// configured in a corresponding run profile
|
||||
#[command(external_subcommand)]
|
||||
@@ -220,6 +225,26 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Config {} => {
|
||||
let config_yaml = serde_yaml::to_string(&config)
|
||||
.with_context(|| "failed to serialize existing configuration")?;
|
||||
let new_config = Editor::new()
|
||||
.edit(&config_yaml)
|
||||
.with_context(|| "unable to process user changes")?;
|
||||
if new_config.is_none() {
|
||||
println!("✗ No changes made to configuration");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new_config = new_config.unwrap();
|
||||
let new_config: Config = serde_yaml::from_str(&new_config)
|
||||
.with_context(|| "failed to parse updated configuration")?;
|
||||
new_config
|
||||
.validate()
|
||||
.with_context(|| "updated configuration is invalid")?;
|
||||
persist_config_file(&new_config)?;
|
||||
println!("✓ Configuration updated successfully");
|
||||
}
|
||||
Commands::Sync {} => {
|
||||
secrets_provider.sync().await.map(|_| {
|
||||
if cli.output.is_none() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gman::config::{Config, get_config_file_path};
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
@@ -60,6 +62,28 @@ pub fn get_log_path() -> PathBuf {
|
||||
dir.join("gman.log")
|
||||
}
|
||||
|
||||
pub fn persist_config_file(config: &Config) -> Result<()> {
|
||||
let config_path =
|
||||
get_config_file_path().with_context(|| "unable to determine config file path")?;
|
||||
let ext = config_path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("");
|
||||
if ext.eq_ignore_ascii_case("yml") || ext.eq_ignore_ascii_case("yaml") {
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let s = serde_yaml::to_string(config)?;
|
||||
fs::write(&config_path, s)
|
||||
.with_context(|| format!("failed to write {}", config_path.display()))?;
|
||||
} else {
|
||||
confy::store("gman", "config", config)
|
||||
.with_context(|| "failed to save updated config via confy")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::get_log_path;
|
||||
|
||||
+20
-1
@@ -19,6 +19,8 @@
|
||||
//! };
|
||||
//! rc.validate().unwrap();
|
||||
//! ```
|
||||
|
||||
use collections::HashSet;
|
||||
use crate::providers::local::LocalProvider;
|
||||
use crate::providers::{SecretProvider, SupportedProvider};
|
||||
use anyhow::{Context, Result};
|
||||
@@ -28,7 +30,7 @@ use serde_with::serde_as;
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
use std::{collections, env, fs};
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
#[skip_serializing_none]
|
||||
@@ -182,6 +184,7 @@ impl ProviderConfig {
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[validate(schema(function = "default_provider_exists"))]
|
||||
#[validate(schema(function = "providers_names_are_unique"))]
|
||||
pub struct Config {
|
||||
pub default_provider: Option<String>,
|
||||
#[validate(length(min = 1))]
|
||||
@@ -211,6 +214,22 @@ fn default_provider_exists(config: &Config) -> Result<(), ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn providers_names_are_unique(config: &Config) -> Result<(), ValidationError> {
|
||||
let mut names = HashSet::new();
|
||||
for provider in &config.providers {
|
||||
if let Some(name) = &provider.name {
|
||||
if !names.insert(name) {
|
||||
let mut err = ValidationError::new("duplicate_provider_name");
|
||||
err.message = Some(Cow::Borrowed(
|
||||
"Provider names must be unique; duplicate found",
|
||||
));
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -116,8 +116,7 @@ fn resolve_git_username(git: &Path, name: Option<&String>) -> Result<String> {
|
||||
return Ok(name.to_string());
|
||||
}
|
||||
|
||||
run_git_config_capture(git, &["config", "user.name"])
|
||||
.with_context(|| "unable to determine git username")
|
||||
default_git_username(git)
|
||||
}
|
||||
|
||||
fn resolve_git_email(git: &Path, email: Option<&String>) -> Result<String> {
|
||||
@@ -126,11 +125,10 @@ fn resolve_git_email(git: &Path, email: Option<&String>) -> Result<String> {
|
||||
return Ok(email.to_string());
|
||||
}
|
||||
|
||||
run_git_config_capture(git, &["config", "user.email"])
|
||||
.with_context(|| "unable to determine git user email")
|
||||
default_git_email(git)
|
||||
}
|
||||
|
||||
fn resolve_git(override_path: Option<&PathBuf>) -> Result<PathBuf> {
|
||||
pub(in crate::providers) fn resolve_git(override_path: Option<&PathBuf>) -> Result<PathBuf> {
|
||||
debug!("Resolving git executable");
|
||||
if let Some(p) = override_path {
|
||||
return Ok(p.to_path_buf());
|
||||
@@ -141,7 +139,19 @@ fn resolve_git(override_path: Option<&PathBuf>) -> Result<PathBuf> {
|
||||
Ok(PathBuf::from("git"))
|
||||
}
|
||||
|
||||
fn ensure_git_available(git: &Path) -> Result<()> {
|
||||
pub(in crate::providers) fn default_git_username(git: &Path) -> Result<String> {
|
||||
debug!("Checking for default git username");
|
||||
run_git_config_capture(git, &["config", "user.name"])
|
||||
.with_context(|| "unable to determine git user name")
|
||||
}
|
||||
|
||||
pub(in crate::providers) fn default_git_email(git: &Path) -> Result<String> {
|
||||
debug!("Checking for default git username");
|
||||
run_git_config_capture(git, &["config", "user.email"])
|
||||
.with_context(|| "unable to determine git user email")
|
||||
}
|
||||
|
||||
pub(in crate::providers) fn ensure_git_available(git: &Path) -> Result<()> {
|
||||
let ok = Command::new(git)
|
||||
.arg("--version")
|
||||
.stdout(Stdio::null())
|
||||
|
||||
+52
-2
@@ -6,7 +6,10 @@ use std::{env, fs};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::config::{Config, get_config_file_path, load_config};
|
||||
use crate::providers::git_sync::{SyncOpts, repo_name_from_url, sync_and_push};
|
||||
use crate::providers::git_sync::{
|
||||
SyncOpts, default_git_email, default_git_username, ensure_git_available, repo_name_from_url,
|
||||
resolve_git, sync_and_push,
|
||||
};
|
||||
use crate::providers::{SecretProvider, SupportedProvider};
|
||||
use crate::{
|
||||
ARGON_M_COST_KIB, ARGON_P, ARGON_T_COST, HEADER, KDF, KEY_LEN, NONCE_LEN, SALT_LEN, VERSION,
|
||||
@@ -156,6 +159,8 @@ impl SecretProvider for LocalProvider {
|
||||
|
||||
async fn sync(&mut self) -> Result<()> {
|
||||
let mut config_changed = false;
|
||||
let git = resolve_git(self.git_executable.as_ref())?;
|
||||
ensure_git_available(&git)?;
|
||||
|
||||
if self.git_branch.is_none() {
|
||||
config_changed = true;
|
||||
@@ -189,6 +194,39 @@ impl SecretProvider for LocalProvider {
|
||||
self.git_remote_url = Some(remote);
|
||||
}
|
||||
|
||||
if self.git_user_name.is_none() {
|
||||
config_changed = true;
|
||||
debug!("Prompting user git user name");
|
||||
let default_user_name = default_git_username(&git)?.trim().to_string();
|
||||
let branch: String = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter git user name")
|
||||
.default(default_user_name)
|
||||
.interact_text()?;
|
||||
|
||||
self.git_user_name = Some(branch);
|
||||
}
|
||||
|
||||
if self.git_user_email.is_none() {
|
||||
config_changed = true;
|
||||
debug!("Prompting user git email");
|
||||
let default_user_name = default_git_email(&git)?.trim().to_string();
|
||||
let branch: String = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter git user email")
|
||||
.validate_with({
|
||||
|s: &String| {
|
||||
if s.contains('@') {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("not a valid email address".to_string())
|
||||
}
|
||||
}
|
||||
})
|
||||
.default(default_user_name)
|
||||
.interact_text()?;
|
||||
|
||||
self.git_user_email = Some(branch);
|
||||
}
|
||||
|
||||
if config_changed {
|
||||
self.persist_git_settings_to_config()?;
|
||||
}
|
||||
@@ -223,6 +261,9 @@ impl LocalProvider {
|
||||
if matches_name || target_name.is_none() {
|
||||
provider_def.git_branch = self.git_branch.clone();
|
||||
provider_def.git_remote_url = self.git_remote_url.clone();
|
||||
provider_def.git_user_name = self.git_user_name.clone();
|
||||
provider_def.git_user_email = self.git_user_email.clone();
|
||||
provider_def.git_executable = self.git_executable.clone();
|
||||
|
||||
updated = true;
|
||||
if matches_name {
|
||||
@@ -564,7 +605,7 @@ mod tests {
|
||||
.expect("persist ok");
|
||||
|
||||
let content = fs::read_to_string(&cfg_path).unwrap();
|
||||
let cfg: crate::config::Config = serde_yaml::from_str(&content).unwrap();
|
||||
let cfg: Config = serde_yaml::from_str(&content).unwrap();
|
||||
|
||||
assert_eq!(cfg.default_provider.as_deref(), Some("local"));
|
||||
assert!(cfg.run_configs.is_some());
|
||||
@@ -579,6 +620,15 @@ mod tests {
|
||||
provider_def.git_remote_url.as_deref(),
|
||||
Some("git@github.com:user/repo.git")
|
||||
);
|
||||
assert_eq!(provider_def.git_user_name.as_deref(), Some("Test User"));
|
||||
assert_eq!(
|
||||
provider_def.git_user_email.as_deref(),
|
||||
Some("test@example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
provider_def.git_executable.as_ref(),
|
||||
Some(&PathBuf::from("/usr/bin/git"))
|
||||
);
|
||||
}
|
||||
_ => panic!("expected local provider"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user