paths.rs (13070B)
1 use std::path::PathBuf; 2 3 use radroots_runtime_paths::{ 4 RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths, 5 }; 6 7 use crate::error::RadrootsRuntimeManagerError; 8 use crate::model::RadrootsRuntimeManagementContract; 9 10 #[derive(Debug, Clone, PartialEq, Eq)] 11 pub struct ManagedRuntimeSharedPaths { 12 pub instance_registry_path: PathBuf, 13 pub artifact_cache_dir: PathBuf, 14 pub install_root: PathBuf, 15 pub state_root: PathBuf, 16 pub logs_root: PathBuf, 17 pub run_root: PathBuf, 18 pub secrets_root: PathBuf, 19 } 20 21 #[derive(Debug, Clone, PartialEq, Eq)] 22 pub struct ManagedRuntimeInstancePaths { 23 pub install_dir: PathBuf, 24 pub state_dir: PathBuf, 25 pub logs_dir: PathBuf, 26 pub run_dir: PathBuf, 27 pub secrets_dir: PathBuf, 28 pub pid_file_path: PathBuf, 29 pub stdout_log_path: PathBuf, 30 pub stderr_log_path: PathBuf, 31 pub metadata_path: PathBuf, 32 } 33 34 pub fn resolve_shared_paths( 35 contract: &RadrootsRuntimeManagementContract, 36 resolver: &RadrootsPathResolver, 37 profile: RadrootsPathProfile, 38 overrides: &RadrootsPathOverrides, 39 mode_id: &str, 40 ) -> Result<ManagedRuntimeSharedPaths, RadrootsRuntimeManagerError> { 41 ensure_profile_supported(contract, mode_id, profile)?; 42 let roots = resolver.resolve(profile, overrides)?; 43 let path_spec = contract 44 .paths 45 .get(mode_id) 46 .ok_or_else(|| RadrootsRuntimeManagerError::MissingPathSpec(mode_id.to_string()))?; 47 let root = |root_class: &str, rel: &str| root_class_path(&roots, root_class, rel); 48 49 let instance_registry_path = root( 50 &path_spec.instance_registry_root_class, 51 &path_spec.instance_registry_rel, 52 )?; 53 let artifact_cache_dir = root( 54 &path_spec.artifact_cache_root_class, 55 &path_spec.artifact_cache_rel, 56 )?; 57 let install_root = root(&path_spec.install_root_class, &path_spec.install_root_rel)?; 58 let state_root = root(&path_spec.state_root_class, &path_spec.state_root_rel)?; 59 let logs_root = root(&path_spec.logs_root_class, &path_spec.logs_root_rel)?; 60 let run_root = root(&path_spec.run_root_class, &path_spec.run_root_rel)?; 61 let secrets_root = root( 62 &path_spec.secrets_root_class, 63 &path_spec.secrets_namespace_rel, 64 )?; 65 66 Ok(ManagedRuntimeSharedPaths { 67 instance_registry_path, 68 artifact_cache_dir, 69 install_root, 70 state_root, 71 logs_root, 72 run_root, 73 secrets_root, 74 }) 75 } 76 77 pub fn resolve_instance_paths( 78 shared: &ManagedRuntimeSharedPaths, 79 runtime_id: &str, 80 instance_id: &str, 81 ) -> ManagedRuntimeInstancePaths { 82 let suffix = PathBuf::from(runtime_id).join(instance_id); 83 let install_dir = shared.install_root.join(&suffix); 84 let state_dir = shared.state_root.join(&suffix); 85 let logs_dir = shared.logs_root.join(&suffix); 86 let run_dir = shared.run_root.join(&suffix); 87 let secrets_dir = shared.secrets_root.join(&suffix); 88 89 ManagedRuntimeInstancePaths { 90 install_dir, 91 state_dir: state_dir.clone(), 92 logs_dir: logs_dir.clone(), 93 run_dir: run_dir.clone(), 94 secrets_dir, 95 pid_file_path: run_dir.join("runtime.pid"), 96 stdout_log_path: logs_dir.join("stdout.log"), 97 stderr_log_path: logs_dir.join("stderr.log"), 98 metadata_path: state_dir.join("instance.toml"), 99 } 100 } 101 102 pub fn bootstrap_runtime<'a>( 103 contract: &'a RadrootsRuntimeManagementContract, 104 runtime_id: &str, 105 ) -> Result<&'a crate::model::BootstrapRuntimeContract, RadrootsRuntimeManagerError> { 106 contract 107 .bootstrap 108 .get(runtime_id) 109 .ok_or_else(|| RadrootsRuntimeManagerError::UnknownBootstrapRuntime(runtime_id.to_string())) 110 } 111 112 fn ensure_profile_supported( 113 contract: &RadrootsRuntimeManagementContract, 114 mode_id: &str, 115 profile: RadrootsPathProfile, 116 ) -> Result<(), RadrootsRuntimeManagerError> { 117 let mode = contract 118 .mode 119 .get(mode_id) 120 .ok_or_else(|| RadrootsRuntimeManagerError::UnknownManagementMode(mode_id.to_string()))?; 121 let profile_id = profile.to_string(); 122 if mode 123 .supported_profiles 124 .iter() 125 .any(|entry| entry == &profile_id) 126 { 127 Ok(()) 128 } else { 129 Err(RadrootsRuntimeManagerError::UnsupportedProfile { 130 mode_id: mode_id.to_string(), 131 profile: profile_id, 132 }) 133 } 134 } 135 136 fn root_class_path( 137 roots: &RadrootsPaths, 138 root_class: &str, 139 rel: &str, 140 ) -> Result<PathBuf, RadrootsRuntimeManagerError> { 141 let base = match root_class { 142 "config" => &roots.config, 143 "data" => &roots.data, 144 "cache" => &roots.cache, 145 "logs" => &roots.logs, 146 "run" => &roots.run, 147 "secrets" => &roots.secrets, 148 other => { 149 return Err(RadrootsRuntimeManagerError::UnknownRootClass( 150 other.to_string(), 151 )); 152 } 153 }; 154 Ok(base.join(rel)) 155 } 156 157 #[cfg(test)] 158 mod tests { 159 use std::path::PathBuf; 160 161 use radroots_runtime_paths::{ 162 RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, 163 RadrootsPaths, RadrootsPlatform, 164 }; 165 166 use super::{bootstrap_runtime, resolve_shared_paths, root_class_path}; 167 use crate::{ 168 ManagementPathContract, RadrootsRuntimeManagerError, 169 model::RadrootsRuntimeManagementContract, parse_contract_str, 170 }; 171 172 const CONTRACT: &str = r#" 173 schema = "radroots-runtime-management" 174 schema_version = 1 175 owner_doc = "docs/execution/rcl/radroots-modular-runtime-management-bootstrap-rcl.md" 176 runtime_registry = "registry.toml" 177 distribution_contract = "distribution.toml" 178 capabilities_contract = "capabilities.toml" 179 180 [defaults] 181 instance_cardinality = "single_default_instance" 182 managed_runtime_lookup = "shared_instance_registry" 183 explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true 184 global_path_mutation_forbidden = true 185 186 [management_clients] 187 active = ["cli"] 188 defined = ["community-app-desktop"] 189 190 [managed_runtime_targets] 191 active = ["radrootsd"] 192 defined = ["myc", "rhi"] 193 bootstrap_only = ["hyf"] 194 195 [lifecycle] 196 actions = ["install", "uninstall", "start"] 197 destructive_actions = ["uninstall"] 198 health_states = ["not_installed", "running"] 199 200 [mode.interactive_user_managed] 201 contract_state = "active" 202 platforms = ["linux", "macos", "windows"] 203 supported_profiles = ["interactive_user", "repo_local"] 204 service_manager_integration = false 205 uses_absolute_binary_paths = true 206 requires_explicit_pid_tracking = true 207 requires_explicit_log_tracking = true 208 default_instance_cardinality = "single_default_instance" 209 210 [mode.service_host_managed] 211 contract_state = "defined" 212 platforms = ["linux", "macos", "windows"] 213 supported_profiles = ["service_host"] 214 service_manager_integration = true 215 uses_absolute_binary_paths = true 216 default_instance_cardinality = "single_default_instance" 217 218 [paths.interactive_user_managed] 219 shared_namespace = "shared/runtime-manager" 220 instance_registry_root_class = "config" 221 instance_registry_rel = "shared/runtime-manager/instances.toml" 222 artifact_cache_root_class = "cache" 223 artifact_cache_rel = "shared/runtime-manager/artifacts" 224 install_root_class = "data" 225 install_root_rel = "shared/runtime-manager/installs" 226 state_root_class = "data" 227 state_root_rel = "shared/runtime-manager/state" 228 logs_root_class = "logs" 229 logs_root_rel = "shared/runtime-manager" 230 run_root_class = "run" 231 run_root_rel = "shared/runtime-manager" 232 secrets_root_class = "secrets" 233 secrets_namespace_rel = "shared/runtime-manager" 234 235 [instance_metadata] 236 required_fields = ["runtime_id"] 237 optional_fields = ["notes"] 238 239 [bootstrap.radrootsd] 240 runtime_id = "radrootsd" 241 management_mode = "interactive_user_managed" 242 default_instance_id = "local" 243 install_strategy = "archive_unpack" 244 config_format = "toml" 245 requires_bootstrap_secret = true 246 requires_config_bootstrap = true 247 requires_signer_provider = false 248 health_surface = "jsonrpc_status" 249 preferred_cli_binding = true 250 "#; 251 252 fn contract() -> RadrootsRuntimeManagementContract { 253 parse_contract_str(CONTRACT).expect("parse contract") 254 } 255 256 fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) { 257 let rendered = err.to_string(); 258 for part in parts { 259 assert!( 260 rendered.contains(part), 261 "expected `{rendered}` to contain `{part}`" 262 ); 263 } 264 } 265 266 fn linux_resolver() -> RadrootsPathResolver { 267 RadrootsPathResolver::new( 268 RadrootsPlatform::Linux, 269 RadrootsHostEnvironment { 270 home_dir: Some(PathBuf::from("/home/treesap")), 271 ..RadrootsHostEnvironment::default() 272 }, 273 ) 274 } 275 276 #[test] 277 fn bootstrap_lookup_reports_unknown_runtime() { 278 let err = bootstrap_runtime(&contract(), "missing-runtime").expect_err("missing runtime"); 279 assert_error_contains(&err, &["missing-runtime", "no bootstrap entry"]); 280 } 281 282 #[test] 283 fn resolve_shared_paths_reports_unknown_management_mode() { 284 let err = resolve_shared_paths( 285 &contract(), 286 &linux_resolver(), 287 RadrootsPathProfile::InteractiveUser, 288 &RadrootsPathOverrides::default(), 289 "missing-mode", 290 ) 291 .expect_err("missing mode should fail"); 292 assert_error_contains(&err, &["management mode `missing-mode`"]); 293 } 294 295 #[test] 296 fn resolve_shared_paths_reports_unsupported_profile() { 297 let err = resolve_shared_paths( 298 &contract(), 299 &linux_resolver(), 300 RadrootsPathProfile::ServiceHost, 301 &RadrootsPathOverrides::default(), 302 "interactive_user_managed", 303 ) 304 .expect_err("service_host should be unsupported for interactive mode"); 305 assert_error_contains(&err, &["interactive_user_managed", "service_host"]); 306 } 307 308 #[test] 309 fn resolve_shared_paths_reports_missing_path_spec() { 310 let mut contract = contract(); 311 contract.paths.remove("interactive_user_managed"); 312 313 let err = resolve_shared_paths( 314 &contract, 315 &linux_resolver(), 316 RadrootsPathProfile::InteractiveUser, 317 &RadrootsPathOverrides::default(), 318 "interactive_user_managed", 319 ) 320 .expect_err("missing path spec should fail"); 321 assert_error_contains( 322 &err, 323 &["interactive_user_managed", "no shared path specification"], 324 ); 325 } 326 327 #[test] 328 fn resolve_shared_paths_reports_unknown_root_class() { 329 let mutators: &[fn(&mut ManagementPathContract)] = &[ 330 |paths| paths.instance_registry_root_class = "bogus".to_string(), 331 |paths| paths.artifact_cache_root_class = "bogus".to_string(), 332 |paths| paths.install_root_class = "bogus".to_string(), 333 |paths| paths.state_root_class = "bogus".to_string(), 334 |paths| paths.logs_root_class = "bogus".to_string(), 335 |paths| paths.run_root_class = "bogus".to_string(), 336 |paths| paths.secrets_root_class = "bogus".to_string(), 337 ]; 338 339 for mutate in mutators { 340 let mut contract = contract(); 341 mutate( 342 contract 343 .paths 344 .get_mut("interactive_user_managed") 345 .expect("path spec"), 346 ); 347 348 let err = resolve_shared_paths( 349 &contract, 350 &linux_resolver(), 351 RadrootsPathProfile::InteractiveUser, 352 &RadrootsPathOverrides::default(), 353 "interactive_user_managed", 354 ) 355 .expect_err("unknown root class should fail"); 356 assert_error_contains(&err, &["unknown root class `bogus`"]); 357 } 358 } 359 360 #[test] 361 fn root_class_path_maps_all_known_classes() { 362 let roots = RadrootsPaths { 363 config: PathBuf::from("/roots/config"), 364 data: PathBuf::from("/roots/data"), 365 cache: PathBuf::from("/roots/cache"), 366 logs: PathBuf::from("/roots/logs"), 367 run: PathBuf::from("/roots/run"), 368 secrets: PathBuf::from("/roots/secrets"), 369 }; 370 371 assert_eq!( 372 root_class_path(&roots, "config", "a/b").expect("config root"), 373 PathBuf::from("/roots/config/a/b") 374 ); 375 assert_eq!( 376 root_class_path(&roots, "data", "a/b").expect("data root"), 377 PathBuf::from("/roots/data/a/b") 378 ); 379 assert_eq!( 380 root_class_path(&roots, "cache", "a/b").expect("cache root"), 381 PathBuf::from("/roots/cache/a/b") 382 ); 383 assert_eq!( 384 root_class_path(&roots, "logs", "a/b").expect("logs root"), 385 PathBuf::from("/roots/logs/a/b") 386 ); 387 assert_eq!( 388 root_class_path(&roots, "run", "a/b").expect("run root"), 389 PathBuf::from("/roots/run/a/b") 390 ); 391 assert_eq!( 392 root_class_path(&roots, "secrets", "a/b").expect("secrets root"), 393 PathBuf::from("/roots/secrets/a/b") 394 ); 395 } 396 }