terraphim_settings/
lib.rs1use directories::ProjectDirs;
2use serde::de::{self, Deserializer};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use twelf::reexports::toml;
7use twelf::{config, Layer};
8
9#[cfg(feature = "onepassword")]
10use terraphim_onepassword_cli::{OnePasswordLoader, SecretLoader};
11
12#[derive(thiserror::Error, Debug)]
13pub enum Error {
14 #[error("config error: {0}")]
15 ConfigError(#[from] twelf::Error),
16 #[error("io error: {0}")]
17 IoError(#[from] std::io::Error),
18 #[error("env error: {0}")]
19 EnvError(#[from] std::env::VarError),
20 #[cfg(feature = "onepassword")]
21 #[error("1Password error: {0}")]
22 OnePasswordError(#[from] terraphim_onepassword_cli::OnePasswordError),
23}
24
25pub type DeviceSettingsResult<T> = std::result::Result<T, Error>;
28
29pub const DEFAULT_CONFIG_PATH: &str = ".config";
31
32pub const DEFAULT_SETTINGS: &str = include_str!("../default/settings_local_dev.toml");
34
35fn deserialize_bool_from_string<'de, D>(deserializer: D) -> Result<bool, D::Error>
36where
37 D: Deserializer<'de>,
38{
39 #[derive(Deserialize)]
41 #[serde(untagged)]
42 enum BoolOrString {
43 Bool(bool),
44 String(String),
45 }
46
47 match BoolOrString::deserialize(deserializer)? {
48 BoolOrString::Bool(b) => Ok(b),
49 BoolOrString::String(s) => match s.to_lowercase().as_str() {
50 "true" => Ok(true),
51 "false" => Ok(false),
52 _ => Err(de::Error::custom(format!("invalid boolean value: {}", s))),
53 },
54 }
55}
56
57#[config]
63#[derive(Debug, Serialize, Clone)]
64pub struct DeviceSettings {
65 pub server_hostname: String,
67 pub api_endpoint: String,
69 #[serde(deserialize_with = "deserialize_bool_from_string")]
71 pub initialized: bool,
72 pub default_data_path: String,
74 pub profiles: HashMap<String, HashMap<String, String>>,
76}
77
78impl Default for DeviceSettings {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl DeviceSettings {
85 pub fn new() -> Self {
87 Self::load_from_env_and_file(None).unwrap_or_else(|e| {
88 log::warn!(
89 "Failed to load device settings from file: {:?}, using defaults",
90 e
91 );
92 Self::default_embedded()
93 })
94 }
95
96 #[cfg(feature = "onepassword")]
98 pub async fn load_with_onepassword(config_path: Option<PathBuf>) -> DeviceSettingsResult<Self> {
99 log::info!("Loading device settings with 1Password integration...");
100 let config_path = config_path.unwrap_or_else(Self::default_config_path);
101
102 log::debug!("Settings path: {:?}", config_path);
103 let config_file = init_config_file(&config_path)?;
104 log::debug!("Loading config_file: {:?}", config_file);
105
106 let raw_config = std::fs::read_to_string(&config_file)?;
108
109 let loader = OnePasswordLoader::new();
111 let processed_config = if loader.is_available().await {
112 log::info!("1Password CLI available, processing secrets...");
113 loader.process_config(&raw_config).await?
114 } else {
115 log::warn!("1Password CLI not available, using raw configuration");
116 raw_config
117 };
118
119 let settings: DeviceSettings = toml::from_str(&processed_config).map_err(|e| {
121 Error::IoError(std::io::Error::other(format!("TOML parsing error: {}", e)))
122 })?;
123
124 log::info!("Successfully loaded settings with 1Password integration");
125 Ok(settings)
126 }
127
128 #[cfg(feature = "onepassword")]
130 pub async fn process_config_with_secrets(config: &str) -> DeviceSettingsResult<String> {
131 let loader = OnePasswordLoader::new();
132 if loader.is_available().await {
133 Ok(loader.process_config(config).await?)
134 } else {
135 log::warn!("1Password CLI not available, returning raw configuration");
136 Ok(config.to_string())
137 }
138 }
139
140 pub fn default_embedded() -> Self {
143 use std::collections::HashMap;
144
145 let mut profiles = HashMap::new();
146
147 let mut memory_profile = HashMap::new();
149 memory_profile.insert("type".to_string(), "memory".to_string());
150 profiles.insert("memory".to_string(), memory_profile);
151
152 let mut sqlite_profile = HashMap::new();
153 sqlite_profile.insert("type".to_string(), "sqlite".to_string());
154 sqlite_profile.insert(
155 "datadir".to_string(),
156 "/tmp/terraphim_sqlite_embedded".to_string(),
157 );
158 sqlite_profile.insert(
159 "connection_string".to_string(),
160 "/tmp/terraphim_sqlite_embedded/terraphim.db".to_string(),
161 );
162 sqlite_profile.insert("table".to_string(), "terraphim_kv".to_string());
163 profiles.insert("sqlite".to_string(), sqlite_profile);
164
165 Self {
166 server_hostname: "127.0.0.1:8000".to_string(),
167 api_endpoint: "http://localhost:8000/api".to_string(),
168 initialized: true,
169 default_data_path: "/tmp/terraphim_embedded".to_string(),
170 profiles,
171 }
172 }
173 pub fn default_config_path() -> PathBuf {
177 if let Some(proj_dirs) = ProjectDirs::from("com", "aks", "terraphim") {
178 let config_dir = proj_dirs.config_dir();
179 config_dir.to_path_buf()
180 } else {
181 PathBuf::from(DEFAULT_CONFIG_PATH)
182 }
183 }
184
185 pub fn load_from_env_and_file(config_path: Option<PathBuf>) -> DeviceSettingsResult<Self> {
188 log::info!("Loading device settings...");
189 let config_path = match config_path {
190 Some(path) => path,
191 None => DeviceSettings::default_config_path(),
192 };
193
194 log::debug!("Settings path: {:?}", config_path);
195 let config_file = init_config_file(&config_path)?;
196 log::debug!("Loading config_file: {:?}", config_file);
197
198 Ok(DeviceSettings::with_layers(&[
199 Layer::Toml(config_file),
200 Layer::Env(Some(String::from("TERRAPHIM_"))),
201 ])?)
202 }
203 pub fn update_initialized_flag(
204 &mut self,
205 settings_path: Option<PathBuf>,
206 initialized: bool,
207 ) -> Result<(), Error> {
208 let settings_path = settings_path.unwrap_or_else(Self::default_config_path);
209 let settings_path = settings_path.join("settings.toml");
210 self.initialized = initialized;
211 self.save(&settings_path)?;
212 Ok(())
213 }
214
215 pub fn save(&self, path: &PathBuf) -> Result<(), Error> {
217 log::info!("Saving device settings to: {:?}", path);
218 self.save_to_file(path)?;
219 Ok(())
220 }
221
222 fn save_to_file(&self, path: &PathBuf) -> Result<(), Error> {
224 let serialized_settings =
225 toml::to_string_pretty(self).map_err(|e| Error::IoError(std::io::Error::other(e)))?;
226
227 std::fs::write(path, serialized_settings).map_err(Error::IoError)?;
228
229 Ok(())
230 }
231}
232
233fn init_config_file(path: &PathBuf) -> Result<PathBuf, std::io::Error> {
235 if !path.exists() {
236 std::fs::create_dir_all(path)?;
237 }
238 let config_file = path.join("settings.toml");
239 if !config_file.exists() {
240 log::info!("Initializing default config file at: {:?}", path);
241 std::fs::write(&config_file, DEFAULT_SETTINGS)?;
242 } else {
243 log::debug!("Config file exists at: {:?}", config_file);
244 }
245 Ok(config_file)
246}
247
248#[cfg(test)]
251mod tests {
252 use super::*;
253 use test_log::test;
254
255 use envtestkit::lock::lock_test;
256 use envtestkit::set_env;
257 use std::ffi::OsString;
258
259 #[test]
260 fn test_env_variable() {
261 let _lock = lock_test();
262 let _test = set_env(OsString::from("TERRAPHIM_PROFILE_S3_REGION"), "us-west-1");
263 let _test2 = set_env(
264 OsString::from("TERRAPHIM_PROFILE_S3_ENABLE_VIRTUAL_HOST_STYLE"),
265 "on",
266 );
267
268 log::debug!("Env: {:?}", std::env::var("TERRAPHIM_PROFILE_S3_REGION"));
269 let config =
270 DeviceSettings::load_from_env_and_file(Some(PathBuf::from("./test_settings/")));
271
272 log::debug!("Config: {:?}", config);
273 log::debug!(
274 "Region: {:?}",
275 config
276 .as_ref()
277 .unwrap()
278 .profiles
279 .get("s3")
280 .unwrap()
281 .get("region")
282 .unwrap()
283 );
284
285 assert_eq!(
286 config
287 .unwrap()
288 .profiles
289 .get("s3")
290 .unwrap()
291 .get("region")
292 .unwrap(),
293 &String::from("us-west-1")
294 );
295 }
296
297 #[test]
298 fn test_update_initialized_flag() {
299 let test_config_path = PathBuf::from("./test_settings/");
300
301 let mut config =
303 DeviceSettings::load_from_env_and_file(Some(test_config_path.clone())).unwrap();
304 config.initialized = false;
305 assert!(!config.initialized);
306
307 config
309 .update_initialized_flag(Some(test_config_path.clone()), true)
310 .unwrap();
311
312 let config_copy =
314 DeviceSettings::load_from_env_and_file(Some(test_config_path.clone())).unwrap();
315 assert!(config_copy.initialized);
316 }
317}