radrootsd

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

paths.rs (15042B)


      1 use std::path::{Path, PathBuf};
      2 
      3 use anyhow::{Result, bail};
      4 use serde::Serialize;
      5 
      6 const RADROOTSD_RUNTIME_ID: &str = "radrootsd";
      7 const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml";
      8 const DEFAULT_SERVICE_IDENTITY_FILE_NAME: &str = "identity.secret.json";
      9 const TRANSPORT_PUBLISH_DATABASE_FILE_NAME: &str = "transport_publish.sqlite";
     10 const RADROOTSD_PATHS_PROFILE_ENV: &str = "RADROOTSD_PATHS_PROFILE";
     11 const RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV: &str = "RADROOTSD_PATHS_REPO_LOCAL_ROOT";
     12 const RADROOTSD_DEFAULT_SHARED_SECRET_BACKEND: &str = "encrypted_file";
     13 const RADROOTSD_ALLOWED_PROFILES: [&str; 3] = ["interactive_user", "service_host", "repo_local"];
     14 const RADROOTSD_ALLOWED_SHARED_SECRET_BACKENDS: [&str; 1] = ["encrypted_file"];
     15 const SUBORDINATE_PATH_OVERRIDE_SOURCE: &str = "config_artifact";
     16 const SUBORDINATE_PATH_OVERRIDE_KEYS: [&str; 2] = [
     17     "config.service.logs_dir",
     18     "config.transport_publish.database_path",
     19 ];
     20 
     21 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     22 #[allow(dead_code)]
     23 pub(crate) enum Platform {
     24     Linux,
     25     Macos,
     26     Windows,
     27 }
     28 
     29 #[derive(Debug, Clone, Default, PartialEq, Eq)]
     30 pub(crate) struct HostEnvironment {
     31     pub(crate) home_dir: Option<PathBuf>,
     32     pub(crate) appdata_dir: Option<PathBuf>,
     33     pub(crate) localappdata_dir: Option<PathBuf>,
     34     pub(crate) programdata_dir: Option<PathBuf>,
     35 }
     36 
     37 impl HostEnvironment {
     38     fn current() -> Self {
     39         Self {
     40             home_dir: std::env::var_os("HOME").map(PathBuf::from),
     41             appdata_dir: std::env::var_os("APPDATA").map(PathBuf::from),
     42             localappdata_dir: std::env::var_os("LOCALAPPDATA").map(PathBuf::from),
     43             programdata_dir: std::env::var_os("PROGRAMDATA").map(PathBuf::from),
     44         }
     45     }
     46 }
     47 
     48 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     49 pub(crate) enum PathProfile {
     50     InteractiveUser,
     51     ServiceHost,
     52     RepoLocal,
     53 }
     54 
     55 impl PathProfile {
     56     fn parse(value: &str) -> Result<Self> {
     57         match value {
     58             "interactive_user" => Ok(Self::InteractiveUser),
     59             "service_host" => Ok(Self::ServiceHost),
     60             "repo_local" => Ok(Self::RepoLocal),
     61             _ => bail!("unknown radrootsd path profile `{value}`"),
     62         }
     63     }
     64 
     65     const fn as_str(self) -> &'static str {
     66         match self {
     67             Self::InteractiveUser => "interactive_user",
     68             Self::ServiceHost => "service_host",
     69             Self::RepoLocal => "repo_local",
     70         }
     71     }
     72 }
     73 
     74 #[derive(Debug, Clone)]
     75 pub(crate) struct PathResolver {
     76     platform: Platform,
     77     environment: HostEnvironment,
     78 }
     79 
     80 impl PathResolver {
     81     pub(crate) const fn new(platform: Platform, environment: HostEnvironment) -> Self {
     82         Self {
     83             platform,
     84             environment,
     85         }
     86     }
     87 
     88     pub(crate) fn current() -> Self {
     89         #[cfg(target_os = "windows")]
     90         let platform = Platform::Windows;
     91         #[cfg(target_os = "macos")]
     92         let platform = Platform::Macos;
     93         #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
     94         let platform = Platform::Linux;
     95         Self::new(platform, HostEnvironment::current())
     96     }
     97 
     98     fn roots(&self, profile: PathProfile, repo_local_root: Option<&Path>) -> Result<RuntimeRoots> {
     99         match profile {
    100             PathProfile::RepoLocal => repo_local_root
    101                 .map(RuntimeRoots::from_base)
    102                 .ok_or_else(|| anyhow::anyhow!("repo_local requires an explicit root")),
    103             PathProfile::ServiceHost => match self.platform {
    104                 Platform::Linux | Platform::Macos => Ok(RuntimeRoots {
    105                     config: PathBuf::from("/etc/radroots"),
    106                     data: PathBuf::from("/var/lib/radroots"),
    107                     logs: PathBuf::from("/var/log/radroots"),
    108                     secrets: PathBuf::from("/etc/radroots/secrets"),
    109                 }),
    110                 Platform::Windows => {
    111                     let base = self
    112                         .environment
    113                         .programdata_dir
    114                         .as_deref()
    115                         .ok_or_else(|| anyhow::anyhow!("PROGRAMDATA is required"))?
    116                         .join("Radroots");
    117                     Ok(RuntimeRoots {
    118                         config: base.join("config"),
    119                         data: base.join("data"),
    120                         logs: base.join("logs"),
    121                         secrets: base.join("secrets"),
    122                     })
    123                 }
    124             },
    125             PathProfile::InteractiveUser => match self.platform {
    126                 Platform::Linux | Platform::Macos => {
    127                     let base = self
    128                         .environment
    129                         .home_dir
    130                         .as_deref()
    131                         .ok_or_else(|| anyhow::anyhow!("HOME is required"))?
    132                         .join(".radroots");
    133                     Ok(RuntimeRoots::from_base(base.as_path()))
    134                 }
    135                 Platform::Windows => {
    136                     let roaming = self
    137                         .environment
    138                         .appdata_dir
    139                         .as_deref()
    140                         .ok_or_else(|| anyhow::anyhow!("APPDATA is required"))?
    141                         .join("Radroots");
    142                     let local = self
    143                         .environment
    144                         .localappdata_dir
    145                         .as_deref()
    146                         .ok_or_else(|| anyhow::anyhow!("LOCALAPPDATA is required"))?
    147                         .join("Radroots");
    148                     Ok(RuntimeRoots {
    149                         config: roaming.join("config"),
    150                         data: local.join("data"),
    151                         logs: local.join("logs"),
    152                         secrets: roaming.join("secrets"),
    153                     })
    154                 }
    155             },
    156         }
    157     }
    158 }
    159 
    160 #[derive(Debug, Clone)]
    161 struct RuntimeRoots {
    162     config: PathBuf,
    163     data: PathBuf,
    164     logs: PathBuf,
    165     secrets: PathBuf,
    166 }
    167 
    168 impl RuntimeRoots {
    169     fn from_base(base: &Path) -> Self {
    170         Self {
    171             config: base.join("config"),
    172             data: base.join("data"),
    173             logs: base.join("logs"),
    174             secrets: base.join("secrets"),
    175         }
    176     }
    177 
    178     fn service(self) -> Self {
    179         Self {
    180             config: self.config.join("services").join(RADROOTSD_RUNTIME_ID),
    181             data: self.data.join("services").join(RADROOTSD_RUNTIME_ID),
    182             logs: self.logs.join("services").join(RADROOTSD_RUNTIME_ID),
    183             secrets: self.secrets.join("services").join(RADROOTSD_RUNTIME_ID),
    184         }
    185     }
    186 }
    187 
    188 #[derive(Debug, Clone, PartialEq, Eq)]
    189 pub(crate) struct RuntimePathSelection {
    190     pub(crate) profile: PathProfile,
    191     pub(crate) repo_local_root: Option<PathBuf>,
    192     profile_source: String,
    193     repo_local_root_source: Option<String>,
    194 }
    195 
    196 impl RuntimePathSelection {
    197     #[cfg(test)]
    198     pub(crate) fn caller(profile: PathProfile, repo_local_root: Option<PathBuf>) -> Self {
    199         Self {
    200             profile,
    201             repo_local_root,
    202             profile_source: "caller".to_owned(),
    203             repo_local_root_source: None,
    204         }
    205     }
    206 
    207     fn from_env() -> Result<Self> {
    208         let profile_value = std::env::var(RADROOTSD_PATHS_PROFILE_ENV).ok();
    209         let profile = profile_value
    210             .as_deref()
    211             .map(PathProfile::parse)
    212             .transpose()?
    213             .unwrap_or(PathProfile::InteractiveUser);
    214         let repo_local_root = std::env::var_os(RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV)
    215             .filter(|value| !value.is_empty())
    216             .map(PathBuf::from);
    217         if profile == PathProfile::RepoLocal && repo_local_root.is_none() {
    218             bail!("{RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV} is required for repo_local");
    219         }
    220         Ok(Self {
    221             profile,
    222             repo_local_root,
    223             profile_source: if profile_value.is_some() {
    224                 RADROOTSD_PATHS_PROFILE_ENV.to_owned()
    225             } else {
    226                 "default".to_owned()
    227             },
    228             repo_local_root_source: std::env::var_os(RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV)
    229                 .map(|_| RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV.to_owned()),
    230         })
    231     }
    232 }
    233 
    234 #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
    235 pub struct RadrootsdRuntimePathOverrideContractOutput {
    236     pub profile_source: String,
    237     pub root_source: String,
    238     pub repo_local_root: Option<PathBuf>,
    239     pub repo_local_root_source: Option<String>,
    240     pub subordinate_path_override_source: String,
    241     pub subordinate_path_override_keys: Vec<String>,
    242 }
    243 
    244 #[derive(Debug, Clone, PartialEq, Eq)]
    245 pub(crate) struct RadrootsdRuntimePaths {
    246     pub(crate) config_path: PathBuf,
    247     pub(crate) logs_dir: PathBuf,
    248     pub(crate) identity_path: PathBuf,
    249     pub(crate) transport_publish_database_path: PathBuf,
    250 }
    251 
    252 #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
    253 pub struct RadrootsdRuntimeContractOutput {
    254     pub active_profile: String,
    255     pub allowed_profiles: Vec<String>,
    256     pub path_overrides: RadrootsdRuntimePathOverrideContractOutput,
    257     pub default_shared_secret_backend: String,
    258     pub allowed_shared_secret_backends: Vec<String>,
    259     pub canonical_config_path: PathBuf,
    260     pub canonical_logs_dir: PathBuf,
    261     pub canonical_identity_path: PathBuf,
    262     pub canonical_transport_publish_database_path: PathBuf,
    263 }
    264 
    265 pub(crate) fn process_path_selection() -> Result<(PathProfile, Option<PathBuf>)> {
    266     let selection = RuntimePathSelection::from_env()?;
    267     Ok((selection.profile, selection.repo_local_root))
    268 }
    269 
    270 pub(crate) fn resolve_runtime_paths_with_resolver(
    271     resolver: &PathResolver,
    272     profile: PathProfile,
    273     repo_local_root: Option<&Path>,
    274 ) -> Result<RadrootsdRuntimePaths> {
    275     let roots = resolver.roots(profile, repo_local_root)?.service();
    276     Ok(RadrootsdRuntimePaths {
    277         config_path: roots.config.join(DEFAULT_CONFIG_FILE_NAME),
    278         logs_dir: roots.logs,
    279         identity_path: roots.secrets.join(DEFAULT_SERVICE_IDENTITY_FILE_NAME),
    280         transport_publish_database_path: roots.data.join(TRANSPORT_PUBLISH_DATABASE_FILE_NAME),
    281     })
    282 }
    283 
    284 pub(crate) fn default_runtime_paths_for_process() -> Result<RadrootsdRuntimePaths> {
    285     let (profile, repo_local_root) = process_path_selection()?;
    286     resolve_runtime_paths_with_resolver(
    287         &PathResolver::current(),
    288         profile,
    289         repo_local_root.as_deref(),
    290     )
    291 }
    292 
    293 pub(crate) fn default_transport_publish_database_path() -> PathBuf {
    294     default_runtime_paths_for_process()
    295         .expect("resolve canonical radrootsd runtime paths")
    296         .transport_publish_database_path
    297 }
    298 
    299 pub fn default_config_path_for_process() -> Result<PathBuf> {
    300     Ok(default_runtime_paths_for_process()?.config_path)
    301 }
    302 
    303 pub fn default_identity_path_for_process() -> Result<PathBuf> {
    304     Ok(default_runtime_paths_for_process()?.identity_path)
    305 }
    306 
    307 pub fn runtime_contract_for_process() -> Result<RadrootsdRuntimeContractOutput> {
    308     let selection = RuntimePathSelection::from_env()?;
    309     runtime_contract_with_selection(&PathResolver::current(), &selection)
    310 }
    311 
    312 pub(crate) fn runtime_contract_with_selection(
    313     resolver: &PathResolver,
    314     selection: &RuntimePathSelection,
    315 ) -> Result<RadrootsdRuntimeContractOutput> {
    316     let paths = resolve_runtime_paths_with_resolver(
    317         resolver,
    318         selection.profile,
    319         selection.repo_local_root.as_deref(),
    320     )?;
    321     Ok(RadrootsdRuntimeContractOutput {
    322         active_profile: selection.profile.as_str().to_owned(),
    323         allowed_profiles: RADROOTSD_ALLOWED_PROFILES
    324             .into_iter()
    325             .map(str::to_owned)
    326             .collect(),
    327         path_overrides: RadrootsdRuntimePathOverrideContractOutput {
    328             profile_source: selection.profile_source.clone(),
    329             root_source: if selection.profile == PathProfile::RepoLocal {
    330                 "explicit_repo_local_root".to_owned()
    331             } else {
    332                 "host_defaults".to_owned()
    333             },
    334             repo_local_root: selection.repo_local_root.clone(),
    335             repo_local_root_source: selection.repo_local_root_source.clone(),
    336             subordinate_path_override_source: SUBORDINATE_PATH_OVERRIDE_SOURCE.to_owned(),
    337             subordinate_path_override_keys: SUBORDINATE_PATH_OVERRIDE_KEYS
    338                 .into_iter()
    339                 .map(str::to_owned)
    340                 .collect(),
    341         },
    342         default_shared_secret_backend: RADROOTSD_DEFAULT_SHARED_SECRET_BACKEND.to_owned(),
    343         allowed_shared_secret_backends: RADROOTSD_ALLOWED_SHARED_SECRET_BACKENDS
    344             .into_iter()
    345             .map(str::to_owned)
    346             .collect(),
    347         canonical_config_path: paths.config_path,
    348         canonical_logs_dir: paths.logs_dir,
    349         canonical_identity_path: paths.identity_path,
    350         canonical_transport_publish_database_path: paths.transport_publish_database_path,
    351     })
    352 }
    353 
    354 #[cfg(test)]
    355 mod tests {
    356     use std::path::PathBuf;
    357 
    358     use super::{
    359         HostEnvironment, PathProfile, PathResolver, Platform, RadrootsdRuntimeContractOutput,
    360         RuntimePathSelection, default_config_path_for_process, runtime_contract_for_process,
    361         runtime_contract_with_selection,
    362     };
    363 
    364     fn linux_resolver() -> PathResolver {
    365         PathResolver::new(
    366             Platform::Linux,
    367             HostEnvironment {
    368                 home_dir: Some(PathBuf::from("/home/treesap")),
    369                 ..HostEnvironment::default()
    370             },
    371         )
    372     }
    373 
    374     #[test]
    375     fn process_path_entrypoints_remain_linked_in_test_builds() {
    376         let _default_config_path: fn() -> anyhow::Result<PathBuf> = default_config_path_for_process;
    377         let _runtime_contract: fn() -> anyhow::Result<RadrootsdRuntimeContractOutput> =
    378             runtime_contract_for_process;
    379     }
    380 
    381     #[test]
    382     fn runtime_contract_output_contains_canonical_runtime_paths() {
    383         let contract = runtime_contract_with_selection(
    384             &linux_resolver(),
    385             &RuntimePathSelection::caller(PathProfile::InteractiveUser, None),
    386         )
    387         .expect("contract");
    388 
    389         assert_eq!(contract.active_profile, "interactive_user");
    390         assert_eq!(
    391             contract.allowed_profiles,
    392             ["interactive_user", "service_host", "repo_local"]
    393         );
    394         assert_eq!(contract.path_overrides.root_source, "host_defaults");
    395         assert_eq!(
    396             contract.canonical_config_path,
    397             PathBuf::from("/home/treesap/.radroots/config/services/radrootsd/config.toml")
    398         );
    399         assert_eq!(
    400             contract.canonical_logs_dir,
    401             PathBuf::from("/home/treesap/.radroots/logs/services/radrootsd")
    402         );
    403         assert_eq!(
    404             contract.canonical_identity_path,
    405             PathBuf::from(
    406                 "/home/treesap/.radroots/secrets/services/radrootsd/identity.secret.json"
    407             )
    408         );
    409         assert_eq!(
    410             contract.canonical_transport_publish_database_path,
    411             PathBuf::from(
    412                 "/home/treesap/.radroots/data/services/radrootsd/transport_publish.sqlite"
    413             )
    414         );
    415     }
    416 }