conventions.rs (15854B)
1 use std::{ 2 fmt, 3 path::{Path, PathBuf}, 4 }; 5 6 use thiserror::Error; 7 8 use crate::{RadrootsRuntimePathsError, RadrootsServiceInstancePaths}; 9 10 pub const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; 11 pub const SERVICE_STATE_DATABASE_FILE_NAME: &str = "state.sqlite"; 12 pub const SERVICE_STATE_LOCK_FILE_NAME: &str = "state.lock"; 13 pub const SERVICE_ADMIN_SOCKET_FILE_NAME: &str = "admin.sock"; 14 pub const SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES: usize = 128; 15 pub const DEFAULT_SHARED_GEONAMES_NAMESPACE_KIND: &str = "shared"; 16 pub const DEFAULT_SHARED_GEONAMES_NAMESPACE_VALUE: &str = "geonames"; 17 pub const DEFAULT_SHARED_GEONAMES_NAMESPACE: &str = "shared/geonames"; 18 pub const DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE_KIND: &str = "shared"; 19 pub const DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE_VALUE: &str = "runtime_store"; 20 pub const DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE: &str = "shared/runtime_store"; 21 pub const DEFAULT_SHARED_RUNTIME_STORE_DB_FILE_NAME: &str = "runtime_store.sqlite"; 22 23 /// A validated service-owned credential artifact filename. 24 #[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 25 pub struct ServiceCredentialArtifactName(String); 26 27 impl ServiceCredentialArtifactName { 28 pub fn new(value: impl AsRef<str>) -> Result<Self, ServiceCredentialArtifactNameError> { 29 let value = value.as_ref(); 30 if value.is_empty() { 31 return Err(ServiceCredentialArtifactNameError::Empty); 32 } 33 if value.len() > SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES { 34 return Err(ServiceCredentialArtifactNameError::TooLong { 35 maximum: SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES, 36 }); 37 } 38 let bytes = value.as_bytes(); 39 let is_alphanumeric = |byte: u8| byte.is_ascii_lowercase() || byte.is_ascii_digit(); 40 if !is_alphanumeric(bytes[0]) || !is_alphanumeric(bytes[bytes.len() - 1]) { 41 return Err(ServiceCredentialArtifactNameError::InvalidBoundary); 42 } 43 if !bytes 44 .iter() 45 .all(|byte| is_alphanumeric(*byte) || matches!(*byte, b'.' | b'-' | b'_')) 46 { 47 return Err(ServiceCredentialArtifactNameError::InvalidCharacter); 48 } 49 Ok(Self(value.to_owned())) 50 } 51 52 #[must_use] 53 pub fn as_str(&self) -> &str { 54 self.0.as_str() 55 } 56 } 57 58 impl AsRef<str> for ServiceCredentialArtifactName { 59 fn as_ref(&self) -> &str { 60 self.as_str() 61 } 62 } 63 64 impl fmt::Debug for ServiceCredentialArtifactName { 65 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 66 formatter.write_str("ServiceCredentialArtifactName([redacted])") 67 } 68 } 69 70 #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] 71 pub enum ServiceCredentialArtifactNameError { 72 #[error("service credential artifact name must not be empty")] 73 Empty, 74 #[error("service credential artifact name exceeds its {maximum}-byte limit")] 75 TooLong { maximum: usize }, 76 #[error( 77 "service credential artifact name must start and end with a lowercase ASCII letter or digit" 78 )] 79 InvalidBoundary, 80 #[error("service credential artifact name contains a forbidden character")] 81 InvalidCharacter, 82 } 83 84 /// Fixed common artifacts derived for one validated service instance. 85 /// 86 /// Callers cannot override the fixed artifact filenames or roots: 87 /// 88 /// ```compile_fail 89 /// use radroots_runtime_paths::RadrootsServiceInstanceArtifacts; 90 /// 91 /// let _ = RadrootsServiceInstanceArtifacts { 92 /// config: "/tmp/alternate.toml".into(), 93 /// state_database: "/tmp/alternate.sqlite".into(), 94 /// state_lock: "/tmp/alternate.lock".into(), 95 /// admin_socket: "/tmp/alternate.sock".into(), 96 /// }; 97 /// ``` 98 #[derive(Clone, PartialEq, Eq)] 99 pub struct RadrootsServiceInstanceArtifacts { 100 config: PathBuf, 101 state_database: PathBuf, 102 state_lock: PathBuf, 103 admin_socket: PathBuf, 104 } 105 106 impl fmt::Debug for RadrootsServiceInstanceArtifacts { 107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 108 formatter.write_str("RadrootsServiceInstanceArtifacts([redacted])") 109 } 110 } 111 112 impl RadrootsServiceInstanceArtifacts { 113 fn from_instance_paths(paths: &RadrootsServiceInstancePaths) -> Self { 114 Self { 115 config: paths.config().join(DEFAULT_CONFIG_FILE_NAME), 116 state_database: paths.state().join(SERVICE_STATE_DATABASE_FILE_NAME), 117 state_lock: paths.state().join(SERVICE_STATE_LOCK_FILE_NAME), 118 admin_socket: paths.run().join(SERVICE_ADMIN_SOCKET_FILE_NAME), 119 } 120 } 121 122 #[must_use] 123 pub fn config(&self) -> &Path { 124 &self.config 125 } 126 127 #[must_use] 128 pub fn state_database(&self) -> &Path { 129 &self.state_database 130 } 131 132 #[must_use] 133 pub fn state_lock(&self) -> &Path { 134 &self.state_lock 135 } 136 137 #[must_use] 138 pub fn admin_socket(&self) -> &Path { 139 &self.admin_socket 140 } 141 } 142 143 #[must_use] 144 pub fn default_service_instance_artifacts( 145 paths: &RadrootsServiceInstancePaths, 146 ) -> RadrootsServiceInstanceArtifacts { 147 RadrootsServiceInstanceArtifacts::from_instance_paths(paths) 148 } 149 150 #[must_use] 151 pub fn service_credential_artifact_path( 152 paths: &RadrootsServiceInstancePaths, 153 name: &ServiceCredentialArtifactName, 154 ) -> PathBuf { 155 paths.secrets().join(name.as_str()) 156 } 157 158 #[must_use] 159 pub fn default_shared_runtime_store_root_from_data_root(data_root: impl AsRef<Path>) -> PathBuf { 160 data_root 161 .as_ref() 162 .join(DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE_KIND) 163 .join(DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE_VALUE) 164 } 165 166 #[must_use] 167 pub fn default_shared_runtime_store_database_path_from_data_root( 168 data_root: impl AsRef<Path>, 169 ) -> PathBuf { 170 default_shared_runtime_store_root_from_data_root(data_root) 171 .join(DEFAULT_SHARED_RUNTIME_STORE_DB_FILE_NAME) 172 } 173 174 #[must_use] 175 pub fn default_shared_geonames_root_from_cache_root(cache_root: impl AsRef<Path>) -> PathBuf { 176 cache_root 177 .as_ref() 178 .join(DEFAULT_SHARED_GEONAMES_NAMESPACE_KIND) 179 .join(DEFAULT_SHARED_GEONAMES_NAMESPACE_VALUE) 180 } 181 182 #[must_use] 183 pub fn default_shared_geonames_database_file_name(version: &str) -> String { 184 format!("geonames-{version}.db") 185 } 186 187 #[must_use] 188 pub fn default_shared_geonames_database_path_from_cache_root( 189 cache_root: impl AsRef<Path>, 190 version: &str, 191 ) -> PathBuf { 192 default_shared_geonames_root_from_cache_root(cache_root) 193 .join(default_shared_geonames_database_file_name(version)) 194 } 195 196 pub fn default_shared_runtime_store_root_from_shared_accounts_data_root( 197 shared_accounts_data_root: impl AsRef<Path>, 198 ) -> Result<PathBuf, RadrootsRuntimePathsError> { 199 let shared_accounts_data_root = shared_accounts_data_root.as_ref(); 200 let shared_data_root = shared_accounts_data_root.parent().ok_or_else(|| { 201 RadrootsRuntimePathsError::SharedAccountsDataRootMissingParent { 202 path: shared_accounts_data_root.to_path_buf(), 203 } 204 })?; 205 Ok(shared_data_root.join(DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE_VALUE)) 206 } 207 208 pub fn default_shared_runtime_store_database_path_from_shared_accounts_data_root( 209 shared_accounts_data_root: impl AsRef<Path>, 210 ) -> Result<PathBuf, RadrootsRuntimePathsError> { 211 default_shared_runtime_store_root_from_shared_accounts_data_root(shared_accounts_data_root) 212 .map(|root| root.join(DEFAULT_SHARED_RUNTIME_STORE_DB_FILE_NAME)) 213 } 214 215 #[cfg(test)] 216 mod tests { 217 use std::path::PathBuf; 218 219 use super::{ 220 DEFAULT_CONFIG_FILE_NAME, DEFAULT_SHARED_GEONAMES_NAMESPACE, 221 DEFAULT_SHARED_RUNTIME_STORE_DB_FILE_NAME, DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE, 222 SERVICE_ADMIN_SOCKET_FILE_NAME, SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES, 223 SERVICE_STATE_DATABASE_FILE_NAME, SERVICE_STATE_LOCK_FILE_NAME, 224 ServiceCredentialArtifactName, ServiceCredentialArtifactNameError, 225 default_service_instance_artifacts, default_shared_geonames_database_file_name, 226 default_shared_geonames_database_path_from_cache_root, 227 default_shared_geonames_root_from_cache_root, 228 default_shared_runtime_store_database_path_from_data_root, 229 default_shared_runtime_store_database_path_from_shared_accounts_data_root, 230 default_shared_runtime_store_root_from_data_root, 231 default_shared_runtime_store_root_from_shared_accounts_data_root, 232 service_credential_artifact_path, 233 }; 234 use crate::{ 235 InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, 236 RadrootsPlatform, RadrootsRuntimePathsError, RuntimeContext, RuntimeContextBootstrap, 237 RuntimeContextSource, ServiceId, 238 }; 239 240 fn service_context(service: &str, instance: &str) -> RuntimeContext { 241 RuntimeContext::resolve( 242 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 243 RuntimeContextBootstrap::new( 244 RadrootsPathProfile::ServiceHost, 245 None, 246 RuntimeContextSource::SafeDefault, 247 RuntimeContextSource::BootstrapCli, 248 ) 249 .expect("bootstrap"), 250 ServiceId::new(service).expect("service"), 251 InstanceId::new(instance).expect("instance"), 252 ) 253 .expect("context") 254 } 255 256 #[test] 257 fn service_instance_artifacts_use_only_fixed_common_filenames() { 258 assert_eq!(DEFAULT_CONFIG_FILE_NAME, "config.toml"); 259 assert_eq!(SERVICE_STATE_DATABASE_FILE_NAME, "state.sqlite"); 260 assert_eq!(SERVICE_STATE_LOCK_FILE_NAME, "state.lock"); 261 assert_eq!(SERVICE_ADMIN_SOCKET_FILE_NAME, "admin.sock"); 262 263 let context = service_context("myc", "primary"); 264 let artifacts = default_service_instance_artifacts(context.paths()); 265 266 assert_eq!( 267 artifacts.config(), 268 PathBuf::from("/etc/radroots/services/myc/primary/config.toml") 269 ); 270 assert_eq!( 271 artifacts.state_database(), 272 PathBuf::from("/var/lib/radroots/services/myc/primary/state.sqlite") 273 ); 274 assert_eq!( 275 artifacts.state_lock(), 276 PathBuf::from("/var/lib/radroots/services/myc/primary/state.lock") 277 ); 278 assert_eq!( 279 artifacts.admin_socket(), 280 PathBuf::from("/run/radroots/services/myc/primary/admin.sock") 281 ); 282 } 283 284 #[test] 285 fn credential_artifact_names_are_validated_and_remain_outside_state() { 286 let context = service_context("rhi", "default"); 287 let paths = context.paths(); 288 let name = 289 ServiceCredentialArtifactName::new("identity.secret.json").expect("credential name"); 290 assert_eq!(name.as_ref(), "identity.secret.json"); 291 assert_eq!( 292 format!("{name:?}"), 293 "ServiceCredentialArtifactName([redacted])" 294 ); 295 assert!(!format!("{name:?}").contains("identity.secret.json")); 296 let credential = service_credential_artifact_path(paths, &name); 297 assert_eq!( 298 credential, 299 PathBuf::from("/etc/radroots/secrets/services/rhi/default/identity.secret.json") 300 ); 301 assert!(!credential.starts_with(paths.state())); 302 303 let artifacts = default_service_instance_artifacts(paths); 304 let artifacts_debug = format!("{artifacts:?}"); 305 assert_eq!( 306 artifacts_debug, 307 "RadrootsServiceInstanceArtifacts([redacted])" 308 ); 309 assert!(!artifacts_debug.contains("/etc/radroots")); 310 assert!(!artifacts_debug.contains("/var/lib/radroots")); 311 assert!(!artifacts_debug.contains("/run/radroots")); 312 313 let maximum_name = "a".repeat(SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES); 314 assert_eq!( 315 ServiceCredentialArtifactName::new(maximum_name.clone()) 316 .expect("maximum credential name") 317 .as_str(), 318 maximum_name 319 ); 320 321 assert_eq!( 322 ServiceCredentialArtifactName::new(""), 323 Err(ServiceCredentialArtifactNameError::Empty) 324 ); 325 assert_eq!( 326 ServiceCredentialArtifactName::new( 327 "a".repeat(SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES + 1) 328 ), 329 Err(ServiceCredentialArtifactNameError::TooLong { 330 maximum: SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES, 331 }) 332 ); 333 assert_eq!( 334 ServiceCredentialArtifactName::new("a".repeat(4 * 1024 * 1024)), 335 Err(ServiceCredentialArtifactNameError::TooLong { 336 maximum: SERVICE_CREDENTIAL_ARTIFACT_NAME_MAX_BYTES, 337 }) 338 ); 339 for invalid in [ 340 ".", 341 "..", 342 "../identity", 343 "/identity", 344 "identity/secret", 345 r"identity\secret", 346 "Identity", 347 "identity secret", 348 "identity-秘密", 349 ] { 350 assert!( 351 ServiceCredentialArtifactName::new(invalid).is_err(), 352 "accepted invalid credential artifact name `{invalid}`" 353 ); 354 } 355 } 356 357 #[test] 358 fn shared_runtime_store_paths_use_canonical_shared_namespace() { 359 let data_root = PathBuf::from("/repo/infra/local/runtime/radroots/data"); 360 assert_eq!( 361 default_shared_runtime_store_root_from_data_root(&data_root), 362 data_root.join(DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE) 363 ); 364 assert_eq!( 365 default_shared_runtime_store_database_path_from_data_root(&data_root), 366 data_root 367 .join(DEFAULT_SHARED_RUNTIME_STORE_NAMESPACE) 368 .join(DEFAULT_SHARED_RUNTIME_STORE_DB_FILE_NAME) 369 ); 370 } 371 372 #[test] 373 fn shared_geonames_paths_use_canonical_shared_cache_namespace() { 374 let cache_root = PathBuf::from("/repo/infra/local/runtime/radroots/cache"); 375 assert_eq!( 376 default_shared_geonames_root_from_cache_root(&cache_root), 377 cache_root.join(DEFAULT_SHARED_GEONAMES_NAMESPACE) 378 ); 379 assert_eq!( 380 default_shared_geonames_database_file_name("1.0"), 381 "geonames-1.0.db" 382 ); 383 assert_eq!( 384 default_shared_geonames_database_path_from_cache_root(&cache_root, "1.0"), 385 cache_root 386 .join(DEFAULT_SHARED_GEONAMES_NAMESPACE) 387 .join("geonames-1.0.db") 388 ); 389 } 390 391 #[test] 392 fn shared_runtime_store_paths_derive_from_shared_accounts_data_root() { 393 let shared_accounts_data_root = 394 PathBuf::from("/repo/infra/local/runtime/radroots/data/shared/accounts"); 395 assert_eq!( 396 default_shared_runtime_store_root_from_shared_accounts_data_root( 397 &shared_accounts_data_root 398 ) 399 .expect("shared runtime-store root"), 400 PathBuf::from("/repo/infra/local/runtime/radroots/data/shared/runtime_store") 401 ); 402 assert_eq!( 403 default_shared_runtime_store_root_from_shared_accounts_data_root( 404 shared_accounts_data_root.clone() 405 ) 406 .expect("owned shared runtime-store root"), 407 PathBuf::from("/repo/infra/local/runtime/radroots/data/shared/runtime_store") 408 ); 409 assert_eq!( 410 default_shared_runtime_store_database_path_from_shared_accounts_data_root( 411 &shared_accounts_data_root 412 ) 413 .expect("shared runtime-store database path"), 414 PathBuf::from( 415 "/repo/infra/local/runtime/radroots/data/shared/runtime_store/runtime_store.sqlite" 416 ) 417 ); 418 419 assert_eq!( 420 default_shared_runtime_store_root_from_shared_accounts_data_root(PathBuf::from("/")) 421 .expect_err("root path has no parent"), 422 RadrootsRuntimePathsError::SharedAccountsDataRootMissingParent { 423 path: PathBuf::from("/") 424 } 425 ); 426 } 427 }