use crate::{ConfigEntry, ConfigValue, Environment, Result, VersionControl};
use llm_config_crypto::{decrypt, encrypt, SecretKey};
use llm_config_storage::file::FileStorage;
use chrono::Utc;
use std::path::Path;
pub struct ConfigManager {
storage: FileStorage,
version_control: VersionControl,
encryption_key: Option<SecretKey>,
}
impl ConfigManager {
pub fn new(storage_path: impl AsRef<Path>) -> Result<Self> {
let storage = FileStorage::new(storage_path)?;
let version_control = VersionControl::new(storage.clone());
Ok(Self {
storage,
version_control,
encryption_key: None,
})
}
pub fn with_encryption_key(mut self, key: SecretKey) -> Self {
self.encryption_key = Some(key);
self
}
pub fn get(
&self,
namespace: &str,
key: &str,
env: Environment,
) -> Result<Option<ConfigEntry>> {
let mut entry = self.storage.get(namespace, key, env)?;
if let Some(ref mut config) = entry {
if let Some(ref key) = self.encryption_key {
self.decrypt_entry(config, key)?;
}
}
Ok(entry)
}
pub fn get_with_overrides(
&self,
namespace: &str,
key: &str,
env: Environment,
) -> Result<Option<ConfigValue>> {
let mut value = self.storage.get(namespace, key, Environment::Base)?
.map(|e| e.value);
let envs = match env {
Environment::Base => vec![],
Environment::Development => vec![Environment::Development],
Environment::Staging => vec![Environment::Development, Environment::Staging],
Environment::Production => vec![
Environment::Development,
Environment::Staging,
Environment::Production,
],
Environment::Edge => vec![Environment::Edge],
};
for override_env in envs {
if let Some(override_entry) = self.storage.get(namespace, key, override_env)? {
value = Some(override_entry.value);
}
}
if let Some(ConfigValue::Secret(ref encrypted)) = value {
if let Some(ref key) = self.encryption_key {
let plaintext = decrypt(key, encrypted)?;
let plaintext_str = String::from_utf8(plaintext)
.map_err(|e| crate::ConfigError::ValidationError(e.to_string()))?;
return Ok(Some(ConfigValue::String(plaintext_str)));
}
}
Ok(value)
}
pub fn set(
&self,
namespace: impl Into<String>,
key: impl Into<String>,
value: ConfigValue,
env: Environment,
user: impl Into<String>,
) -> Result<ConfigEntry> {
let namespace = namespace.into();
let key_str = key.into();
let user = user.into();
let existing = self.storage.get(&namespace, &key_str, env)?;
let mut entry = if let Some(mut existing_entry) = existing {
existing_entry.value = value;
existing_entry.version += 1;
existing_entry.metadata.updated_at = Utc::now();
existing_entry.metadata.updated_by = user;
existing_entry
} else {
let mut entry = ConfigEntry::new(namespace.clone(), key_str.clone(), value, env);
entry.metadata.created_by = user.clone();
entry.metadata.updated_by = user;
entry
};
if let Some(ref key) = self.encryption_key {
self.encrypt_entry(&mut entry, key)?;
}
self.storage.set(entry.clone())?;
self.version_control.create_snapshot(&entry, Some("Configuration updated".to_string()))?;
Ok(entry)
}
pub fn set_secret(
&self,
namespace: impl Into<String>,
key: impl Into<String>,
plaintext: impl AsRef<[u8]>,
env: Environment,
user: impl Into<String>,
) -> Result<ConfigEntry> {
let encryption_key = self.encryption_key.as_ref()
.ok_or_else(|| crate::ConfigError::ValidationError(
"Encryption key not configured".to_string()
))?;
let encrypted = encrypt(encryption_key, plaintext.as_ref(), None)?;
let value = ConfigValue::Secret(encrypted);
self.set(namespace, key, value, env, user)
}
pub fn get_secret(
&self,
namespace: &str,
key: &str,
env: Environment,
) -> Result<Option<Vec<u8>>> {
let encryption_key = self.encryption_key.as_ref()
.ok_or_else(|| crate::ConfigError::ValidationError(
"Encryption key not configured".to_string()
))?;
let entry = match self.storage.get(namespace, key, env)? {
Some(e) => e,
None => return Ok(None),
};
match entry.value {
ConfigValue::Secret(ref encrypted_data) => {
let plaintext = decrypt(encryption_key, encrypted_data)?;
Ok(Some(plaintext))
}
_ => Err(crate::ConfigError::ValidationError(
"Not a secret value".to_string()
))
}
}
pub fn list(&self, namespace: &str, env: Environment) -> Result<Vec<ConfigEntry>> {
let mut entries = self.storage.list(namespace, env)?;
if let Some(ref key) = self.encryption_key {
for entry in &mut entries {
self.decrypt_entry(entry, key)?;
}
}
Ok(entries)
}
pub fn delete(&self, namespace: &str, key: &str, env: Environment) -> Result<bool> {
Ok(self.storage.delete(namespace, key, env)?)
}
pub fn get_history(
&self,
namespace: &str,
key: &str,
env: Environment,
) -> Result<Vec<llm_config_storage::VersionEntry>> {
self.version_control.get_history(namespace, key, env)
}
pub fn rollback(
&self,
namespace: &str,
key: &str,
env: Environment,
version: u64,
) -> Result<Option<ConfigEntry>> {
self.version_control.rollback(namespace, key, env, version)
}
fn decrypt_entry(&self, entry: &mut ConfigEntry, key: &SecretKey) -> Result<()> {
if let ConfigValue::Secret(ref encrypted) = entry.value {
let plaintext = decrypt(key, encrypted)?;
let plaintext_str = String::from_utf8(plaintext)
.map_err(|e| crate::ConfigError::ValidationError(e.to_string()))?;
entry.value = ConfigValue::String(plaintext_str);
}
Ok(())
}
fn encrypt_entry(&self, _entry: &mut ConfigEntry, _key: &SecretKey) -> Result<()> {
Ok(())
}
pub fn export_all(&self, export_path: impl AsRef<Path>) -> Result<usize> {
Ok(self.storage.export_all(export_path)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use llm_config_crypto::Algorithm;
use tempfile::TempDir;
#[test]
fn test_manager_creation() {
let temp_dir = TempDir::new().unwrap();
let manager = ConfigManager::new(temp_dir.path()).unwrap();
assert!(manager.encryption_key.is_none());
}
#[test]
fn test_set_and_get() {
let temp_dir = TempDir::new().unwrap();
let manager = ConfigManager::new(temp_dir.path()).unwrap();
let entry = manager
.set(
"test/ns",
"config.key",
ConfigValue::String("value".to_string()),
Environment::Development,
"test-user",
)
.unwrap();
assert_eq!(entry.version, 1);
let retrieved = manager
.get("test/ns", "config.key", Environment::Development)
.unwrap()
.unwrap();
assert_eq!(retrieved.key, "config.key");
assert_eq!(retrieved.value.as_str().unwrap(), "value");
}
#[test]
fn test_environment_overrides() {
let temp_dir = TempDir::new().unwrap();
let manager = ConfigManager::new(temp_dir.path()).unwrap();
manager
.set(
"test/ns",
"key",
ConfigValue::String("base".to_string()),
Environment::Base,
"user",
)
.unwrap();
manager
.set(
"test/ns",
"key",
ConfigValue::String("production".to_string()),
Environment::Production,
"user",
)
.unwrap();
let value = manager
.get_with_overrides("test/ns", "key", Environment::Production)
.unwrap()
.unwrap();
assert_eq!(value.as_str().unwrap(), "production");
let dev_value = manager
.get_with_overrides("test/ns", "key", Environment::Development)
.unwrap()
.unwrap();
assert_eq!(dev_value.as_str().unwrap(), "base");
}
#[test]
fn test_secret_encryption() {
let temp_dir = TempDir::new().unwrap();
let key = SecretKey::generate(Algorithm::Aes256Gcm).unwrap();
let manager = ConfigManager::new(temp_dir.path())
.unwrap()
.with_encryption_key(key);
let secret_value = b"my-secret-password";
manager
.set_secret(
"test/ns",
"db.password",
secret_value,
Environment::Production,
"admin",
)
.unwrap();
let retrieved = manager
.get("test/ns", "db.password", Environment::Production)
.unwrap()
.unwrap();
assert_eq!(
retrieved.value.as_str().unwrap(),
std::str::from_utf8(secret_value).unwrap()
);
}
#[test]
fn test_list_configs() {
let temp_dir = TempDir::new().unwrap();
let manager = ConfigManager::new(temp_dir.path()).unwrap();
manager
.set("test/ns", "key1", ConfigValue::String("val1".to_string()), Environment::Development, "user")
.unwrap();
manager
.set("test/ns", "key2", ConfigValue::String("val2".to_string()), Environment::Development, "user")
.unwrap();
let list = manager.list("test/ns", Environment::Development).unwrap();
assert_eq!(list.len(), 2);
}
#[test]
fn test_delete() {
let temp_dir = TempDir::new().unwrap();
let manager = ConfigManager::new(temp_dir.path()).unwrap();
manager
.set("test/ns", "key", ConfigValue::String("val".to_string()), Environment::Development, "user")
.unwrap();
let deleted = manager.delete("test/ns", "key", Environment::Development).unwrap();
assert!(deleted);
let retrieved = manager.get("test/ns", "key", Environment::Development).unwrap();
assert!(retrieved.is_none());
}
#[test]
fn test_versioning() {
let temp_dir = TempDir::new().unwrap();
let manager = ConfigManager::new(temp_dir.path()).unwrap();
manager
.set("test/ns", "key", ConfigValue::String("v1".to_string()), Environment::Development, "user")
.unwrap();
manager
.set("test/ns", "key", ConfigValue::String("v2".to_string()), Environment::Development, "user")
.unwrap();
let history = manager.get_history("test/ns", "key", Environment::Development).unwrap();
assert!(history.len() >= 2);
let rolled_back = manager
.rollback("test/ns", "key", Environment::Development, 1)
.unwrap()
.unwrap();
assert_eq!(rolled_back.value.as_str().unwrap(), "v1");
}
}