use crate::errors::{ConfGuardContext, ConfGuardError, ConfGuardResult};
use crate::sops::manager::SopsConfig;
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 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(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 = "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("Settings::new() uses hardcoded defaults - cannot fail"))
})
}
pub fn read_global() -> std::sync::RwLockReadGuard<'static, Settings> {
Self::global()
.read()
.expect("settings lock not poisoned - no panics in critical section")
}
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("settings lock not poisoned - no panics in critical section");
*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("read SOPS config file")?;
let config: SopsConfig = toml::from_str(&config_str).context("parse SOPS config")?;
self.sops = Some(config);
Ok(())
}
}
pub fn settings() -> std::sync::RwLockReadGuard<'static, Settings> {
Settings::read_global()
}
pub fn settings_with_override(base_dir_override: Option<&Path>) -> Settings {
match base_dir_override {
Some(path) => {
let base = path.to_string_lossy().to_string();
let mut s = Settings::read_global().clone();
s.confguard_base_dir = base.clone();
s.guarded = format!("{}/guarded", base);
s
}
None => Settings::read_global().clone(),
}
}
pub fn 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 = 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, init_test_setup, setup_test_dir, TEST_VERSION,
};
use rstest::rstest;
use std::env;
#[rstest]
fn print_settings() {
init_test_setup();
let settings = settings();
println!("{:?}", settings);
}
#[rstest]
fn test_default_settings() {
init_test_setup();
let settings = 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 base = default_base();
assert_eq!(base, "/test/home/xxx/rs-cg");
env::remove_var("HOME");
let fallback = default_base();
assert_eq!(fallback, "/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 = 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 = 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 = confguard_config_path(Some(&override_path))?;
assert_eq!(result, override_path);
Ok(())
}
}