radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

paths.rs (9878B)


      1 use std::path::{Path, PathBuf};
      2 
      3 use anyhow::{Context, Result};
      4 use radroots_runtime_paths::{
      5     DEFAULT_CONFIG_FILE_NAME, DEFAULT_SERVICE_IDENTITY_FILE_NAME, RadrootsLegacyPathCandidate,
      6     RadrootsMigrationReport, RadrootsPathProfile, RadrootsPathResolver,
      7     RadrootsRuntimeMigrationContract, RadrootsRuntimePathSelection,
      8     RadrootsRuntimeSelectionContract, RadrootsRuntimeSelectionOverrideContract,
      9     inspect_legacy_paths, runtime_migration_contract,
     10 };
     11 use serde::Serialize;
     12 
     13 const RADROOTSD_RUNTIME_ID: &str = "radrootsd";
     14 const PUBLISH_PROXY_DATABASE_FILE_NAME: &str = "publish_proxy.sqlite";
     15 const RADROOTSD_PATHS_PROFILE_ENV: &str = "RADROOTSD_PATHS_PROFILE";
     16 const RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV: &str = "RADROOTSD_PATHS_REPO_LOCAL_ROOT";
     17 const RADROOTSD_DEFAULT_SHARED_SECRET_BACKEND: &str = "encrypted_file";
     18 const RADROOTSD_ALLOWED_PROFILES: [&str; 3] = ["interactive_user", "service_host", "repo_local"];
     19 const RADROOTSD_ALLOWED_SHARED_SECRET_BACKENDS: [&str; 1] = ["encrypted_file"];
     20 const SUBORDINATE_PATH_OVERRIDE_SOURCE: &str = "config_artifact";
     21 const SUBORDINATE_PATH_OVERRIDE_KEYS: [&str; 2] = [
     22     "config.service.logs_dir",
     23     "config.publish_proxy.database_path",
     24 ];
     25 const MIGRATION_IMPORT_HINT: &str = "stop the runtime, inspect this legacy path, then perform an explicit import or manual copy into the canonical destination; radrootsd will not move it on startup";
     26 
     27 #[derive(Debug, Clone, PartialEq, Eq)]
     28 pub(crate) struct RadrootsdRuntimePaths {
     29     pub(crate) config_path: PathBuf,
     30     pub(crate) logs_dir: PathBuf,
     31     pub(crate) identity_path: PathBuf,
     32     pub(crate) publish_proxy_database_path: PathBuf,
     33 }
     34 
     35 #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
     36 pub struct RadrootsdRuntimeContractOutput {
     37     pub active_profile: String,
     38     pub allowed_profiles: Vec<String>,
     39     pub path_overrides: RadrootsdRuntimePathOverrideContractOutput,
     40     pub default_shared_secret_backend: String,
     41     pub allowed_shared_secret_backends: Vec<String>,
     42     pub migration: RadrootsdRuntimeMigrationContractOutput,
     43     pub canonical_config_path: PathBuf,
     44     pub canonical_logs_dir: PathBuf,
     45     pub canonical_identity_path: PathBuf,
     46     pub canonical_publish_proxy_database_path: PathBuf,
     47 }
     48 
     49 pub type RadrootsdRuntimeMigrationContractOutput = RadrootsRuntimeMigrationContract;
     50 pub type RadrootsdRuntimePathOverrideContractOutput = RadrootsRuntimeSelectionOverrideContract;
     51 
     52 pub(crate) fn process_path_selection() -> Result<(RadrootsPathProfile, Option<PathBuf>)> {
     53     let selection = process_path_selection_with_sources()?;
     54     Ok((selection.profile, selection.repo_local_root))
     55 }
     56 
     57 fn process_path_selection_with_sources() -> Result<RadrootsRuntimePathSelection> {
     58     RadrootsRuntimePathSelection::from_env(
     59         RADROOTSD_PATHS_PROFILE_ENV,
     60         RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV,
     61         RadrootsPathProfile::InteractiveUser,
     62     )
     63     .map_err(|error| anyhow::anyhow!(error.to_string()))
     64 }
     65 
     66 pub(crate) fn resolve_runtime_paths_with_resolver(
     67     resolver: &RadrootsPathResolver,
     68     profile: RadrootsPathProfile,
     69     repo_local_root: Option<&Path>,
     70 ) -> Result<RadrootsdRuntimePaths> {
     71     let selection =
     72         RadrootsRuntimePathSelection::caller(profile, repo_local_root.map(Path::to_path_buf));
     73     let namespaced = selection
     74         .resolve_service_roots(
     75             resolver,
     76             RADROOTSD_RUNTIME_ID,
     77             RADROOTSD_PATHS_PROFILE_ENV,
     78             RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV,
     79         )
     80         .map_err(|error| anyhow::anyhow!("resolve radrootsd runtime paths: {error}"))?;
     81     Ok(RadrootsdRuntimePaths {
     82         config_path: namespaced.config.join(DEFAULT_CONFIG_FILE_NAME),
     83         logs_dir: namespaced.logs,
     84         identity_path: namespaced.secrets.join(DEFAULT_SERVICE_IDENTITY_FILE_NAME),
     85         publish_proxy_database_path: namespaced.data.join(PUBLISH_PROXY_DATABASE_FILE_NAME),
     86     })
     87 }
     88 
     89 pub(crate) fn default_runtime_paths_for_process() -> Result<RadrootsdRuntimePaths> {
     90     let (profile, repo_local_root) = process_path_selection()?;
     91     resolve_runtime_paths_with_resolver(
     92         &RadrootsPathResolver::current(),
     93         profile,
     94         repo_local_root.as_deref(),
     95     )
     96 }
     97 
     98 pub(crate) fn default_publish_proxy_database_path() -> PathBuf {
     99     default_runtime_paths_for_process()
    100         .expect("resolve canonical radrootsd runtime paths")
    101         .publish_proxy_database_path
    102 }
    103 
    104 #[cfg_attr(test, allow(dead_code))]
    105 pub fn default_config_path_for_process() -> Result<PathBuf> {
    106     Ok(default_runtime_paths_for_process()?.config_path)
    107 }
    108 
    109 pub fn default_identity_path_for_process() -> Result<PathBuf> {
    110     Ok(default_runtime_paths_for_process()?.identity_path)
    111 }
    112 
    113 #[cfg_attr(test, allow(dead_code))]
    114 pub fn runtime_contract_for_process() -> Result<RadrootsdRuntimeContractOutput> {
    115     let selection = process_path_selection_with_sources()?;
    116     runtime_contract_with_selection(&RadrootsPathResolver::current(), &selection)
    117 }
    118 
    119 #[cfg_attr(not(test), allow(dead_code))]
    120 pub(crate) fn runtime_contract_with_resolver(
    121     resolver: &RadrootsPathResolver,
    122     profile: RadrootsPathProfile,
    123     repo_local_root: Option<&Path>,
    124 ) -> Result<RadrootsdRuntimeContractOutput> {
    125     runtime_contract_with_selection(
    126         resolver,
    127         &RadrootsRuntimePathSelection::caller(profile, repo_local_root.map(Path::to_path_buf)),
    128     )
    129 }
    130 
    131 fn runtime_contract_with_selection(
    132     resolver: &RadrootsPathResolver,
    133     selection: &RadrootsRuntimePathSelection,
    134 ) -> Result<RadrootsdRuntimeContractOutput> {
    135     let profile = selection.profile;
    136     let repo_local_root = selection.repo_local_root.as_deref();
    137     let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?;
    138     let base_contract: RadrootsRuntimeSelectionContract = selection.contract(
    139         &RADROOTSD_ALLOWED_PROFILES,
    140         SUBORDINATE_PATH_OVERRIDE_SOURCE,
    141         &SUBORDINATE_PATH_OVERRIDE_KEYS,
    142     );
    143     Ok(RadrootsdRuntimeContractOutput {
    144         active_profile: base_contract.active_profile,
    145         allowed_profiles: base_contract.allowed_profiles,
    146         path_overrides: base_contract.path_overrides,
    147         default_shared_secret_backend: RADROOTSD_DEFAULT_SHARED_SECRET_BACKEND.to_owned(),
    148         allowed_shared_secret_backends: RADROOTSD_ALLOWED_SHARED_SECRET_BACKENDS
    149             .into_iter()
    150             .map(str::to_owned)
    151             .collect(),
    152         migration: runtime_migration_contract(RadrootsMigrationReport::empty()),
    153         canonical_config_path: paths.config_path,
    154         canonical_logs_dir: paths.logs_dir,
    155         canonical_identity_path: paths.identity_path,
    156         canonical_publish_proxy_database_path: paths.publish_proxy_database_path,
    157     })
    158 }
    159 
    160 #[allow(dead_code)]
    161 pub(crate) fn runtime_migration_for_process(
    162     contract: &RadrootsdRuntimeContractOutput,
    163 ) -> Result<RadrootsdRuntimeMigrationContractOutput> {
    164     let current_dir = std::env::current_dir().context("resolve current directory")?;
    165     Ok(runtime_migration_for_current_dir(
    166         contract,
    167         current_dir.as_path(),
    168     ))
    169 }
    170 
    171 pub(crate) fn runtime_migration_for_current_dir(
    172     contract: &RadrootsdRuntimeContractOutput,
    173     current_dir: &Path,
    174 ) -> RadrootsdRuntimeMigrationContractOutput {
    175     let report = inspect_legacy_paths(legacy_path_candidates(contract, current_dir));
    176     migration_contract_output(report)
    177 }
    178 
    179 fn legacy_path_candidates(
    180     contract: &RadrootsdRuntimeContractOutput,
    181     current_dir: &Path,
    182 ) -> Vec<RadrootsLegacyPathCandidate> {
    183     vec![
    184         RadrootsLegacyPathCandidate::new(
    185             "radrootsd_repo_config_v0",
    186             "legacy radrootsd repo-relative config",
    187             current_dir.join(DEFAULT_CONFIG_FILE_NAME),
    188             Some(contract.canonical_config_path.clone()),
    189             MIGRATION_IMPORT_HINT,
    190         ),
    191         RadrootsLegacyPathCandidate::new(
    192             "radrootsd_repo_logs_v0",
    193             "legacy radrootsd repo-relative logs directory",
    194             current_dir.join("logs"),
    195             Some(contract.canonical_logs_dir.clone()),
    196             MIGRATION_IMPORT_HINT,
    197         ),
    198     ]
    199 }
    200 
    201 fn migration_contract_output(
    202     report: RadrootsMigrationReport,
    203 ) -> RadrootsdRuntimeMigrationContractOutput {
    204     runtime_migration_contract(report)
    205 }
    206 
    207 #[cfg(test)]
    208 mod tests {
    209     use std::path::PathBuf;
    210 
    211     use radroots_runtime_paths::{
    212         RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform,
    213     };
    214 
    215     use super::{runtime_contract_with_resolver, runtime_migration_for_current_dir};
    216 
    217     fn linux_resolver() -> RadrootsPathResolver {
    218         RadrootsPathResolver::new(
    219             RadrootsPlatform::Linux,
    220             RadrootsHostEnvironment {
    221                 home_dir: Some(PathBuf::from("/home/treesap")),
    222                 ..RadrootsHostEnvironment::default()
    223             },
    224         )
    225     }
    226 
    227     #[test]
    228     fn runtime_migration_detects_legacy_repo_relative_state_without_moving_it() {
    229         let temp = tempfile::tempdir().expect("tempdir");
    230         std::fs::write(
    231             temp.path().join("config.toml"),
    232             "[metadata]\nname = \"old\"\n",
    233         )
    234         .expect("write old config");
    235         let contract = runtime_contract_with_resolver(
    236             &linux_resolver(),
    237             RadrootsPathProfile::InteractiveUser,
    238             None,
    239         )
    240         .expect("contract");
    241 
    242         let report = runtime_migration_for_current_dir(&contract, temp.path());
    243 
    244         assert_eq!(report.posture, "explicit_operator_import_required");
    245         assert_eq!(report.state, "legacy_state_detected");
    246         assert!(!report.silent_startup_relocation);
    247         assert_eq!(report.detected_legacy_paths.len(), 1);
    248         assert_eq!(
    249             report.detected_legacy_paths[0].id,
    250             "radrootsd_repo_config_v0"
    251         );
    252         assert_eq!(
    253             report.detected_legacy_paths[0].destination,
    254             Some(contract.canonical_config_path)
    255         );
    256     }
    257 }