registry.rs (10146B)
1 use std::fs; 2 use std::path::Path; 3 4 use crate::error::RadrootsRuntimeManagerError; 5 use crate::model::{ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry}; 6 7 pub fn load_registry( 8 path: impl AsRef<Path>, 9 ) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> { 10 load_registry_path(path.as_ref()) 11 } 12 13 fn load_registry_path( 14 path: &Path, 15 ) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> { 16 let raw = match fs::read_to_string(path) { 17 Ok(raw) => raw, 18 Err(err) if err.kind() == std::io::ErrorKind::NotFound => { 19 return Ok(ManagedRuntimeInstanceRegistry::default()); 20 } 21 Err(source) => { 22 return Err(RadrootsRuntimeManagerError::ReadRegistry { 23 path: path.to_path_buf(), 24 source, 25 }); 26 } 27 }; 28 29 toml::from_str::<ManagedRuntimeInstanceRegistry>(&raw).map_err(|source| { 30 RadrootsRuntimeManagerError::ParseRegistry { 31 path: path.to_path_buf(), 32 details: source.to_string(), 33 } 34 }) 35 } 36 37 pub fn save_registry( 38 path: impl AsRef<Path>, 39 registry: &ManagedRuntimeInstanceRegistry, 40 ) -> Result<(), RadrootsRuntimeManagerError> { 41 save_registry_path(path.as_ref(), registry) 42 } 43 44 fn save_registry_path( 45 path: &Path, 46 registry: &ManagedRuntimeInstanceRegistry, 47 ) -> Result<(), RadrootsRuntimeManagerError> { 48 save_registry_path_with(path, registry, toml::to_string_pretty) 49 } 50 51 fn save_registry_path_with( 52 path: &Path, 53 registry: &ManagedRuntimeInstanceRegistry, 54 serializer: fn(&ManagedRuntimeInstanceRegistry) -> Result<String, toml::ser::Error>, 55 ) -> Result<(), RadrootsRuntimeManagerError> { 56 ensure_registry_parent(path)?; 57 58 let raw = serializer(registry) 59 .map_err(|err| RadrootsRuntimeManagerError::SerializeRegistry(err.to_string()))?; 60 fs::write(path, raw).map_err(|source| RadrootsRuntimeManagerError::WriteRegistry { 61 path: path.to_path_buf(), 62 source, 63 }) 64 } 65 66 pub fn upsert_instance( 67 registry: &mut ManagedRuntimeInstanceRegistry, 68 record: ManagedRuntimeInstanceRecord, 69 ) { 70 if let Some(existing) = registry.instances.iter_mut().find(|existing| { 71 existing.runtime_id == record.runtime_id && existing.instance_id == record.instance_id 72 }) { 73 *existing = record; 74 } else { 75 registry.instances.push(record); 76 registry.instances.sort_by(|left, right| { 77 left.runtime_id 78 .cmp(&right.runtime_id) 79 .then_with(|| left.instance_id.cmp(&right.instance_id)) 80 }); 81 } 82 } 83 84 pub fn instance<'a>( 85 registry: &'a ManagedRuntimeInstanceRegistry, 86 runtime_id: &str, 87 instance_id: &str, 88 ) -> Option<&'a ManagedRuntimeInstanceRecord> { 89 registry 90 .instances 91 .iter() 92 .find(|record| record.runtime_id == runtime_id && record.instance_id == instance_id) 93 } 94 95 pub fn remove_instance( 96 registry: &mut ManagedRuntimeInstanceRegistry, 97 runtime_id: &str, 98 instance_id: &str, 99 ) -> Option<ManagedRuntimeInstanceRecord> { 100 let index = registry 101 .instances 102 .iter() 103 .position(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)?; 104 Some(registry.instances.remove(index)) 105 } 106 107 fn ensure_registry_parent(path: &Path) -> Result<(), RadrootsRuntimeManagerError> { 108 let Some(parent) = path.parent() else { 109 return Ok(()); 110 }; 111 if parent.as_os_str().is_empty() { 112 return Ok(()); 113 } 114 fs::create_dir_all(parent).map_err(|source| RadrootsRuntimeManagerError::CreateRegistryParent { 115 path: parent.to_path_buf(), 116 source, 117 }) 118 } 119 120 #[cfg(test)] 121 mod tests { 122 use std::fs; 123 use std::path::{Path, PathBuf}; 124 125 use serde::ser::Error as _; 126 use tempfile::tempdir; 127 128 use super::{ 129 ensure_registry_parent, instance, load_registry, remove_instance, save_registry, 130 save_registry_path_with, upsert_instance, 131 }; 132 use crate::{ 133 ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, 134 RadrootsRuntimeManagerError, 135 }; 136 137 fn sample_record(runtime_id: &str, instance_id: &str) -> ManagedRuntimeInstanceRecord { 138 ManagedRuntimeInstanceRecord { 139 runtime_id: runtime_id.to_string(), 140 instance_id: instance_id.to_string(), 141 management_mode: "interactive_user_managed".to_string(), 142 install_state: ManagedRuntimeInstallState::Configured, 143 binary_path: PathBuf::from("/tmp/radrootsd"), 144 config_path: PathBuf::from("/tmp/config.toml"), 145 logs_path: PathBuf::from("/tmp/logs"), 146 run_path: PathBuf::from("/tmp/run"), 147 installed_version: "0.1.0-alpha.2".to_string(), 148 health_endpoint: Some("jsonrpc_status".to_string()), 149 secret_material_ref: None, 150 last_started_at: None, 151 last_stopped_at: None, 152 notes: Some("test".to_string()), 153 } 154 } 155 156 fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) { 157 let rendered = err.to_string(); 158 for part in parts { 159 assert!( 160 rendered.contains(part), 161 "expected `{rendered}` to contain `{part}`" 162 ); 163 } 164 } 165 166 #[test] 167 fn load_registry_returns_default_when_file_is_missing() { 168 let dir = tempdir().expect("tempdir"); 169 let registry = load_registry(dir.path().join("missing.toml")).expect("missing registry"); 170 assert_eq!(registry, ManagedRuntimeInstanceRegistry::default()); 171 } 172 173 #[test] 174 fn load_registry_reports_read_errors() { 175 let dir = tempdir().expect("tempdir"); 176 let err = load_registry(dir.path()).expect_err("directory should fail"); 177 assert_error_contains( 178 &err, 179 &[ 180 dir.path().to_string_lossy().as_ref(), 181 "read runtime instance registry", 182 ], 183 ); 184 } 185 186 #[test] 187 fn load_registry_reports_parse_errors() { 188 let dir = tempdir().expect("tempdir"); 189 let path = dir.path().join("instances.toml"); 190 fs::write(&path, "not = [valid").expect("write invalid registry"); 191 192 let err = load_registry(&path).expect_err("invalid registry should fail"); 193 assert_error_contains( 194 &err, 195 &[ 196 path.to_string_lossy().as_ref(), 197 "parse runtime instance registry", 198 ], 199 ); 200 } 201 202 #[test] 203 fn save_registry_reports_write_errors() { 204 let dir = tempdir().expect("tempdir"); 205 let path = dir.path().join("registry-dir"); 206 fs::create_dir(&path).expect("create directory target"); 207 208 let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default()) 209 .expect_err("directory path should fail"); 210 assert_error_contains( 211 &err, 212 &[ 213 path.to_string_lossy().as_ref(), 214 "write runtime instance registry", 215 ], 216 ); 217 } 218 219 #[test] 220 fn save_registry_reports_parent_creation_errors() { 221 let dir = tempdir().expect("tempdir"); 222 let file_parent = dir.path().join("occupied"); 223 fs::write(&file_parent, "file").expect("occupied parent"); 224 let path = file_parent.join("instances.toml"); 225 226 let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default()) 227 .expect_err("file parent should fail"); 228 assert_error_contains( 229 &err, 230 &[ 231 file_parent.to_string_lossy().as_ref(), 232 "create runtime instance registry parent", 233 ], 234 ); 235 } 236 237 #[test] 238 fn save_registry_reports_serializer_errors() { 239 let dir = tempdir().expect("tempdir"); 240 let path = dir.path().join("instances.toml"); 241 242 let err = 243 save_registry_path_with(&path, &ManagedRuntimeInstanceRegistry::default(), |_| { 244 Err(toml::ser::Error::custom( 245 "forced registry serializer failure", 246 )) 247 }) 248 .expect_err("serializer should fail"); 249 250 assert_error_contains( 251 &err, 252 &[ 253 "serialize runtime instance registry", 254 "forced registry serializer failure", 255 ], 256 ); 257 } 258 259 #[test] 260 fn ensure_registry_parent_accepts_parentless_relative_paths() { 261 ensure_registry_parent(Path::new("instances.toml")).expect("relative path parentless"); 262 ensure_registry_parent(Path::new("/")).expect("root path parentless"); 263 } 264 265 #[test] 266 fn upsert_instance_replaces_existing_and_sorts_new_records() { 267 let mut registry = ManagedRuntimeInstanceRegistry::default(); 268 upsert_instance(&mut registry, sample_record("radrootsd", "b")); 269 upsert_instance(&mut registry, sample_record("radrootsd", "a")); 270 upsert_instance(&mut registry, sample_record("myc", "a")); 271 272 let mut replacement = sample_record("radrootsd", "b"); 273 replacement.installed_version = "0.2.0".to_string(); 274 upsert_instance(&mut registry, replacement); 275 276 assert_eq!(registry.instances.len(), 3); 277 assert_eq!(registry.instances[0].runtime_id, "myc"); 278 assert_eq!(registry.instances[1].instance_id, "a"); 279 assert_eq!(registry.instances[2].runtime_id, "radrootsd"); 280 assert_eq!(registry.instances[2].instance_id, "b"); 281 assert_eq!(registry.instances[2].installed_version, "0.2.0"); 282 } 283 284 #[test] 285 fn instance_and_remove_instance_handle_missing_and_present_rows() { 286 let mut registry = ManagedRuntimeInstanceRegistry::default(); 287 upsert_instance(&mut registry, sample_record("radrootsd", "local")); 288 289 assert!(instance(®istry, "myc", "local").is_none()); 290 assert!(remove_instance(&mut registry, "myc", "local").is_none()); 291 292 let removed = remove_instance(&mut registry, "radrootsd", "local").expect("remove"); 293 assert_eq!(removed.runtime_id, "radrootsd"); 294 assert!(registry.instances.is_empty()); 295 } 296 }