fix: the vault's init_bare should try to load the provisioned secret_provider from the config file without also interpolating any of the rest of the configuration file. It should only fail if the user has not yet created a configuration file; i.e. done a first-time run.
This commit is contained in:
@@ -731,7 +731,7 @@ fn merge_mcp_json(
|
|||||||
serde_json::to_string_pretty(&merged).context("failed to serialize merged mcp.json")?;
|
serde_json::to_string_pretty(&merged).context("failed to serialize merged mcp.json")?;
|
||||||
write_atomically(&final_path, &serialized)?;
|
write_atomically(&final_path, &serialized)?;
|
||||||
|
|
||||||
let vault = Vault::init_bare();
|
let vault = Vault::init_bare()?;
|
||||||
let (_parsed, missing) = interpolate_secrets(&serialized, &vault)?;
|
let (_parsed, missing) = interpolate_secrets(&serialized, &vault)?;
|
||||||
let mut deduped: Vec<String> = Vec::new();
|
let mut deduped: Vec<String> = Vec::new();
|
||||||
for s in missing {
|
for s in missing {
|
||||||
@@ -860,7 +860,7 @@ fn handle_missing_secrets(missing: &[String]) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_each_secret(missing: &[String]) -> Result<(Vec<String>, Vec<String>)> {
|
fn prompt_for_each_secret(missing: &[String]) -> Result<(Vec<String>, Vec<String>)> {
|
||||||
let mut vault = Vault::init_bare();
|
let mut vault = Vault::init_bare()?;
|
||||||
let mut password_file_ensured = false;
|
let mut password_file_ensured = false;
|
||||||
let mut added = Vec::new();
|
let mut added = Vec::new();
|
||||||
let mut deferred = Vec::new();
|
let mut deferred = Vec::new();
|
||||||
@@ -914,6 +914,62 @@ fn print_secret_summary(added: &[String], deferred: &[String]) {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::utils::get_env_name;
|
||||||
|
use serial_test::serial;
|
||||||
|
use std::env;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
struct TestVaultConfigGuard {
|
||||||
|
dir_key: String,
|
||||||
|
file_key: String,
|
||||||
|
previous_dir: Option<OsString>,
|
||||||
|
previous_file: Option<OsString>,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestVaultConfigGuard {
|
||||||
|
fn new(label: &str) -> Self {
|
||||||
|
let dir_key = get_env_name("config_dir");
|
||||||
|
let file_key = get_env_name("config_file");
|
||||||
|
let previous_dir = env::var_os(&dir_key);
|
||||||
|
let previous_file = env::var_os(&file_key);
|
||||||
|
let unique = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let path = env::temp_dir().join(format!("coyote-vault-test-{label}-{unique}"));
|
||||||
|
fs::create_dir_all(&path).unwrap();
|
||||||
|
let config_path = path.join("config.yaml");
|
||||||
|
fs::write(&config_path, "{}").unwrap();
|
||||||
|
unsafe {
|
||||||
|
env::set_var(&dir_key, &path);
|
||||||
|
env::set_var(&file_key, &config_path);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
dir_key,
|
||||||
|
file_key,
|
||||||
|
previous_dir,
|
||||||
|
previous_file,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestVaultConfigGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
match &self.previous_dir {
|
||||||
|
Some(p) => env::set_var(&self.dir_key, p),
|
||||||
|
None => env::remove_var(&self.dir_key),
|
||||||
|
}
|
||||||
|
match &self.previous_file {
|
||||||
|
Some(p) => env::set_var(&self.file_key, p),
|
||||||
|
None => env::remove_var(&self.file_key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&self.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_url_no_ref() {
|
fn parse_url_no_ref() {
|
||||||
@@ -1253,7 +1309,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[serial]
|
||||||
fn merge_into_empty_local_adds_all_remote_servers() {
|
fn merge_into_empty_local_adds_all_remote_servers() {
|
||||||
|
let _guard = TestVaultConfigGuard::new("merge-empty");
|
||||||
let dir = fresh_temp_dir("merge-empty-");
|
let dir = fresh_temp_dir("merge-empty-");
|
||||||
let remote = dir.join("remote.json");
|
let remote = dir.join("remote.json");
|
||||||
let target = dir.join("target.json");
|
let target = dir.join("target.json");
|
||||||
@@ -1270,7 +1328,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[serial]
|
||||||
fn merge_force_replaces_local_on_conflict() {
|
fn merge_force_replaces_local_on_conflict() {
|
||||||
|
let _guard = TestVaultConfigGuard::new("merge-force");
|
||||||
let dir = fresh_temp_dir("merge-force-");
|
let dir = fresh_temp_dir("merge-force-");
|
||||||
let remote = dir.join("remote.json");
|
let remote = dir.join("remote.json");
|
||||||
let target = dir.join("target.json");
|
let target = dir.join("target.json");
|
||||||
@@ -1336,7 +1396,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
#[serial]
|
||||||
async fn merge_detects_missing_secrets_in_output() {
|
async fn merge_detects_missing_secrets_in_output() {
|
||||||
|
let _guard = TestVaultConfigGuard::new("merge-secret");
|
||||||
let dir = fresh_temp_dir("merge-secret-");
|
let dir = fresh_temp_dir("merge-secret-");
|
||||||
let remote = dir.join("remote.json");
|
let remote = dir.join("remote.json");
|
||||||
let target = dir.join("target.json");
|
let target = dir.join("target.json");
|
||||||
@@ -1352,7 +1414,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[serial]
|
||||||
fn merge_is_idempotent_on_re_run() {
|
fn merge_is_idempotent_on_re_run() {
|
||||||
|
let _guard = TestVaultConfigGuard::new("merge-idempotent");
|
||||||
let dir = fresh_temp_dir("merge-idempotent-");
|
let dir = fresh_temp_dir("merge-idempotent-");
|
||||||
let remote = dir.join("remote.json");
|
let remote = dir.join("remote.json");
|
||||||
let target = dir.join("target.json");
|
let target = dir.join("target.json");
|
||||||
|
|||||||
+1
-1
@@ -685,7 +685,7 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
|
|||||||
|
|
||||||
let provider_choice = prompt_provider_choice()?;
|
let provider_choice = prompt_provider_choice()?;
|
||||||
let mut vault = match &provider_choice {
|
let mut vault = match &provider_choice {
|
||||||
None => Vault::init_bare(),
|
None => Vault::default_local(),
|
||||||
Some(provider) => Vault {
|
Some(provider) => Vault {
|
||||||
provider: provider.clone(),
|
provider: provider.clone(),
|
||||||
},
|
},
|
||||||
|
|||||||
+43
-7
@@ -1,6 +1,9 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
use std::fs::read_to_string;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::config::paths;
|
||||||
pub use utils::create_vault_password_file;
|
pub use utils::create_vault_password_file;
|
||||||
pub use utils::interpolate_secrets;
|
pub use utils::interpolate_secrets;
|
||||||
pub use utils::prompt_provider_choice;
|
pub use utils::prompt_provider_choice;
|
||||||
@@ -14,6 +17,7 @@ use gman::providers::SecretProvider;
|
|||||||
use gman::providers::SupportedProvider;
|
use gman::providers::SupportedProvider;
|
||||||
use gman::providers::local::LocalProvider;
|
use gman::providers::local::LocalProvider;
|
||||||
use inquire::{Password, PasswordDisplayMode, required};
|
use inquire::{Password, PasswordDisplayMode, required};
|
||||||
|
use serde_yaml::Value;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -28,17 +32,49 @@ pub struct Vault {
|
|||||||
pub type GlobalVault = Arc<Vault>;
|
pub type GlobalVault = Arc<Vault>;
|
||||||
|
|
||||||
impl Vault {
|
impl Vault {
|
||||||
pub fn init_bare() -> Self {
|
pub fn init_bare() -> Result<Self> {
|
||||||
let vault_password_file = AppConfig::default().vault_password_file();
|
let config_path = paths::config_file();
|
||||||
let local_provider = LocalProvider {
|
if !config_path.exists() {
|
||||||
password_file: Some(vault_password_file),
|
bail!(
|
||||||
git_branch: None,
|
"Coyote config not found at {}. Run first-run setup before using the vault.",
|
||||||
..LocalProvider::default()
|
config_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let content = read_to_string(&config_path)
|
||||||
|
.with_context(|| format!("failed to read config at {}", config_path.display()))?;
|
||||||
|
let value: Value = serde_yaml::from_str(&content)
|
||||||
|
.with_context(|| format!("failed to parse config at {}", config_path.display()))?;
|
||||||
|
|
||||||
|
let provider = match value.get("secrets_provider") {
|
||||||
|
Some(v) if !v.is_null() => serde_yaml::from_value::<SupportedProvider>(v.clone())
|
||||||
|
.with_context(|| "failed to parse 'secrets_provider' from config")?,
|
||||||
|
_ => {
|
||||||
|
let password_file = value
|
||||||
|
.get("vault_password_file")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| AppConfig::default().vault_password_file());
|
||||||
|
SupportedProvider::Local {
|
||||||
|
provider_def: LocalProvider {
|
||||||
|
password_file: Some(password_file),
|
||||||
|
git_branch: None,
|
||||||
|
..LocalProvider::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Ok(Self { provider })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_local() -> Self {
|
||||||
Self {
|
Self {
|
||||||
provider: SupportedProvider::Local {
|
provider: SupportedProvider::Local {
|
||||||
provider_def: local_provider,
|
provider_def: LocalProvider {
|
||||||
|
password_file: Some(AppConfig::default().vault_password_file()),
|
||||||
|
git_branch: None,
|
||||||
|
..LocalProvider::default()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user