lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }