use crate::sops::manager::SopsConfig;
use crate::errors::{ConfGuardError, ConfGuardResult, ConfGuardContext};
use config::{Config, ConfigError};
use once_cell::sync::{Lazy, OnceCell};
use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::{env, fs};
use tracing::debug;
pub fn get_default_base() -> String {
env::var("HOME")
.map(|home| format!("{}/xxx/rs-cg", home))
.unwrap_or_else(|_| "/tmp/xxx/rs-cg".to_string())
}
pub static DEFAULT_BASE: Lazy<String> = Lazy::new(|| get_default_base());
const DEFAULT_VERSION: i64 = 3;
pub const CONFGUARD_CONFIG_FILE: &str = "confguard.toml";
static SETTINGS: OnceCell<RwLock<Settings>> = OnceCell::new();
#[derive(Debug, Deserialize, Clone)]
pub struct Settings {
#[serde(default = "get_default_base")]
pub confguard_base_dir: String, pub guarded: String, #[serde(default = "default_version")]
pub version: i64,
#[serde(default)]
pub sops: Option<SopsConfig>,
}
fn default_version() -> i64 {
DEFAULT_VERSION
}
impl Settings {
pub fn new() -> Result<Self, ConfigError> {
let config = Config::builder()
.set_default("base_dir", DEFAULT_BASE.as_str())?
.set_default("version", DEFAULT_VERSION)?
.add_source(config::Environment::with_prefix("CONFGUARD"))
.build()?;
let settings = Settings {
confguard_base_dir: config.get_string("base_dir")?,
guarded: format!("{}/guarded", config.get_string("base_dir")?),
version: DEFAULT_VERSION, sops: None, };
debug!("settings: {:?}", settings);
Ok(settings)
}
pub fn global() -> &'static RwLock<Settings> {
SETTINGS.get_or_init(|| RwLock::new(Self::new().expect("Failed to initialize settings")))
}
pub fn read_global() -> std::sync::RwLockReadGuard<'static, Settings> {
Self::global()
.read()
.expect("Failed to acquire settings read lock")
}
pub fn update_global(
new_settings: Settings,
) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'static, Settings>>> {
let mut settings = Self::global().write()?;
*settings = new_settings;
Ok(())
}
pub fn reload() -> Result<(), ConfigError> {
if let Some(lock) = SETTINGS.get() {
let mut settings = lock.write().expect("Failed to acquire settings write lock");
*settings = Self::new()?;
}
Ok(())
}
pub fn load_sops_config(&mut self, config_path: &Path) -> ConfGuardResult<()> {
if !config_path.exists() {
return Ok(());
}
let config_str =
fs::read_to_string(config_path).context("Failed to read SOPS config file")?;
let config: SopsConfig =
toml::from_str(&config_str).context("Failed to parse SOPS config")?;
self.sops = Some(config);
Ok(())
}
}
pub fn get_settings() -> std::sync::RwLockReadGuard<'static, Settings> {
Settings::read_global()
}
pub fn get_confguard_config_path(override_path: Option<impl AsRef<Path>>) -> ConfGuardResult<PathBuf> {
match override_path {
Some(path) => Ok(PathBuf::from(path.as_ref())),
None => {
let settings = get_settings();
let config_path =
PathBuf::from(&settings.confguard_base_dir).join(CONFGUARD_CONFIG_FILE);
if !config_path.exists() {
return Err(ConfGuardError::ConfigNotFoundWithHint {
path: config_path.display().to_string(),
});
}
Ok(config_path)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::testing::{create_file_with_content, setup_test_dir, TEST_VERSION};
use rstest::rstest;
use std::env;
#[rstest]
fn print_settings() {
let settings = get_settings();
println!("{:?}", settings);
}
#[rstest]
fn test_default_settings() {
let settings = get_settings();
println!("{:?}", settings);
assert_eq!(settings.confguard_base_dir, DEFAULT_BASE.as_str());
assert_eq!(settings.version, DEFAULT_VERSION);
}
#[test]
fn test_home_based_default_path() {
let original_home = env::var("HOME").ok();
env::set_var("HOME", "/test/home");
let default_base = get_default_base();
assert_eq!(default_base, "/test/home/xxx/rs-cg");
env::remove_var("HOME");
let fallback_base = get_default_base();
assert_eq!(fallback_base, "/tmp/xxx/rs-cg");
if let Some(home) = original_home {
env::set_var("HOME", home);
}
}
#[test]
fn test_environment_override() {
std::env::remove_var("CONFGUARD_BASE_DIR");
std::env::remove_var("CONFGUARD_VERSION");
std::env::set_var("CONFGUARD_BASE_DIR", "/custom/path");
std::env::set_var("CONFGUARD_VERSION", "3");
let settings = Settings::new().unwrap();
println!("{:?}", settings);
assert_eq!(
settings.confguard_base_dir, "/custom/path",
"Environment variable CONFGUARD_BASE_DIR should override default"
);
assert_eq!(
settings.version, 3,
"Environment variable CONFGUARD_VERSION should override default"
);
std::env::remove_var("CONFGUARD_BASE_DIR");
std::env::remove_var("CONFGUARD_VERSION");
}
#[ignore = "manual test, changes global settings for other tests"]
#[test]
fn test_update_global_settings() {
let new_settings = Settings {
confguard_base_dir: "/new/path".to_string(),
guarded: "/new/path/guarded".to_string(),
version: 4,
sops: None,
};
Settings::update_global(new_settings.clone()).unwrap();
let settings = Settings::read_global();
assert_eq!(settings.confguard_base_dir, "/new/path");
assert_eq!(settings.version, 4);
}
#[rstest]
fn test_get_confguard_config_path_default() -> ConfGuardResult<()> {
let _ = setup_test_dir();
let settings = get_settings();
let config_path = PathBuf::from(&settings.confguard_base_dir).join(CONFGUARD_CONFIG_FILE);
std::fs::create_dir_all(config_path.parent().unwrap())?;
create_file_with_content(&config_path.to_str().unwrap(), "# Test config\n")?;
let result = get_confguard_config_path(None as Option<&Path>)?;
assert_eq!(result, config_path);
Ok(())
}
#[rstest]
fn test_get_confguard_config_path_override() -> ConfGuardResult<()> {
let override_path = PathBuf::from("/custom/path/confguard.toml");
let result = get_confguard_config_path(Some(&override_path))?;
assert_eq!(result, override_path);
Ok(())
}
}