lib

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

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 }