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 }