lib

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

resolve.rs (15368B)


      1 use crate::error::RadrootsRuntimeDistributionError;
      2 use crate::model::{
      3     ArtifactAdapter, RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSpec,
      4 };
      5 use crate::service::{HardenedServiceTarget, ServiceTier1Target};
      6 use crate::service_artifact::{HardenedServiceArtifact, ServiceArtifactSha256};
      7 use radroots_runtime_paths::ServiceId;
      8 
      9 pub const RUNTIME_DISTRIBUTION_SCHEMA: &str = "radroots-runtime-distribution";
     10 pub const RUNTIME_DISTRIBUTION_SCHEMA_VERSION: u32 = 1;
     11 pub const RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES: usize = 1_048_576;
     12 
     13 #[derive(Debug, Clone, PartialEq, Eq)]
     14 pub struct ServiceTargetRequest<'a> {
     15     pub service_id: &'a ServiceId,
     16     pub target_id: &'a str,
     17 }
     18 
     19 #[derive(Debug, Clone, PartialEq, Eq)]
     20 pub struct ResolvedServiceTarget {
     21     service_id: ServiceId,
     22     target: ServiceTier1Target,
     23 }
     24 
     25 #[derive(Debug, Clone, PartialEq, Eq)]
     26 pub struct ServiceArtifactRequest<'a> {
     27     pub service_id: &'a ServiceId,
     28     pub target_id: &'a str,
     29 }
     30 
     31 /// Exact native release artifact metadata for one hardened service target.
     32 #[derive(Clone, PartialEq, Eq)]
     33 pub struct ResolvedServiceArtifact {
     34     service_id: ServiceId,
     35     target: ServiceTier1Target,
     36     version: String,
     37     package_name: String,
     38     binary_name: String,
     39     channel: String,
     40     binary_archive_name: String,
     41     binary_archive_format: &'static str,
     42     binary_archive_member: String,
     43     artifact_manifest_name: String,
     44     checksums_name: String,
     45     checksum_algorithm: &'static str,
     46     checksum_format: &'static str,
     47     release_contract_sha256: ServiceArtifactSha256,
     48     source_lock_sha256: ServiceArtifactSha256,
     49     output_inventory: Vec<String>,
     50 }
     51 
     52 impl core::fmt::Debug for ResolvedServiceArtifact {
     53     fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
     54         formatter
     55             .debug_struct("ResolvedServiceArtifact")
     56             .field("service_id", &self.service_id)
     57             .field("target", &self.target)
     58             .field("version", &self.version)
     59             .field("package_name", &self.package_name)
     60             .field("binary_name", &self.binary_name)
     61             .field("channel", &self.channel)
     62             .field("binary_archive_name", &self.binary_archive_name)
     63             .field("binary_archive_format", &self.binary_archive_format)
     64             .field("binary_archive_member", &self.binary_archive_member)
     65             .field("artifact_manifest_name", &self.artifact_manifest_name)
     66             .field("checksums_name", &self.checksums_name)
     67             .field("checksum_algorithm", &self.checksum_algorithm)
     68             .field("checksum_format", &self.checksum_format)
     69             .field("release_contract_sha256", &"<redacted>")
     70             .field("source_lock_sha256", &"<redacted>")
     71             .field("output_inventory", &self.output_inventory)
     72             .finish()
     73     }
     74 }
     75 
     76 impl ResolvedServiceArtifact {
     77     #[must_use]
     78     pub fn service_id(&self) -> &ServiceId {
     79         &self.service_id
     80     }
     81     #[must_use]
     82     pub const fn target(&self) -> ServiceTier1Target {
     83         self.target
     84     }
     85     #[must_use]
     86     pub fn version(&self) -> &str {
     87         &self.version
     88     }
     89     #[must_use]
     90     pub fn package_name(&self) -> &str {
     91         &self.package_name
     92     }
     93     #[must_use]
     94     pub fn binary_name(&self) -> &str {
     95         &self.binary_name
     96     }
     97     #[must_use]
     98     pub fn channel(&self) -> &str {
     99         &self.channel
    100     }
    101     #[must_use]
    102     pub fn binary_archive_name(&self) -> &str {
    103         &self.binary_archive_name
    104     }
    105     #[must_use]
    106     pub const fn binary_archive_format(&self) -> &'static str {
    107         self.binary_archive_format
    108     }
    109     #[must_use]
    110     pub fn binary_archive_member(&self) -> &str {
    111         &self.binary_archive_member
    112     }
    113     #[must_use]
    114     pub fn artifact_manifest_name(&self) -> &str {
    115         &self.artifact_manifest_name
    116     }
    117     #[must_use]
    118     pub fn checksums_name(&self) -> &str {
    119         &self.checksums_name
    120     }
    121     #[must_use]
    122     pub const fn checksum_algorithm(&self) -> &'static str {
    123         self.checksum_algorithm
    124     }
    125     #[must_use]
    126     pub const fn checksum_format(&self) -> &'static str {
    127         self.checksum_format
    128     }
    129     #[must_use]
    130     pub const fn release_contract_sha256(&self) -> ServiceArtifactSha256 {
    131         self.release_contract_sha256
    132     }
    133     #[must_use]
    134     pub const fn source_lock_sha256(&self) -> ServiceArtifactSha256 {
    135         self.source_lock_sha256
    136     }
    137     #[must_use]
    138     pub fn output_inventory(&self) -> &[String] {
    139         &self.output_inventory
    140     }
    141 }
    142 
    143 impl ResolvedServiceTarget {
    144     #[must_use]
    145     pub fn service_id(&self) -> &ServiceId {
    146         &self.service_id
    147     }
    148 
    149     #[must_use]
    150     pub const fn target(&self) -> ServiceTier1Target {
    151         self.target
    152     }
    153 }
    154 
    155 #[derive(Debug, Clone, PartialEq, Eq)]
    156 pub struct RuntimeArtifactRequest<'a> {
    157     pub runtime_id: &'a str,
    158     pub os: &'a str,
    159     pub arch: &'a str,
    160     pub version: &'a str,
    161     pub channel: Option<&'a str>,
    162 }
    163 
    164 #[derive(Debug, Clone, PartialEq, Eq)]
    165 pub struct ResolvedRuntimeArtifact {
    166     pub runtime_id: String,
    167     pub release_unit: String,
    168     pub package_name: String,
    169     pub binary_name: Option<String>,
    170     pub artifact_adapter: String,
    171     pub channel: String,
    172     pub version: String,
    173     pub target_id: String,
    174     pub os: String,
    175     pub arch: String,
    176     pub archive_format: String,
    177     pub archive_extension: String,
    178     pub artifact_stem: String,
    179     pub artifact_file_name: String,
    180 }
    181 
    182 #[derive(Debug, Clone)]
    183 pub struct RadrootsRuntimeDistributionResolver {
    184     contract: RadrootsRuntimeDistributionContract,
    185 }
    186 
    187 impl RadrootsRuntimeDistributionResolver {
    188     pub fn parse_str(raw: &str) -> Result<Self, RadrootsRuntimeDistributionError> {
    189         if raw.len() > RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES {
    190             return Err(RadrootsRuntimeDistributionError::ContractTooLarge);
    191         }
    192         let contract = toml::from_str::<RadrootsRuntimeDistributionContract>(raw)
    193             .map_err(|_| RadrootsRuntimeDistributionError::Parse)?;
    194         Self::new(contract)
    195     }
    196 
    197     pub fn new(
    198         contract: RadrootsRuntimeDistributionContract,
    199     ) -> Result<Self, RadrootsRuntimeDistributionError> {
    200         if contract.schema != RUNTIME_DISTRIBUTION_SCHEMA {
    201             return Err(RadrootsRuntimeDistributionError::UnexpectedSchema);
    202         }
    203         if contract.schema_version != RUNTIME_DISTRIBUTION_SCHEMA_VERSION {
    204             return Err(RadrootsRuntimeDistributionError::UnexpectedSchemaVersion);
    205         }
    206         if contract.runtime.iter().any(|runtime| {
    207             contract
    208                 .service_targets
    209                 .iter()
    210                 .any(|(_, service)| runtime.id == service.service_id().as_str())
    211         }) {
    212             return Err(RadrootsRuntimeDistributionError::HardenedServiceLegacyArtifactRow);
    213         }
    214         if !contract
    215             .channels
    216             .active
    217             .iter()
    218             .any(|channel| channel == "stable")
    219             || !contract
    220                 .channels
    221                 .defined
    222                 .iter()
    223                 .any(|channel| channel == "stable")
    224         {
    225             return Err(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract);
    226         }
    227         for (service_id, target) in contract.service_targets.iter() {
    228             let artifact = contract
    229                 .service_artifacts
    230                 .get(target.service_id())
    231                 .ok_or(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract)?;
    232             if service_id != artifact.service_id().as_str()
    233                 || target.tier_1_targets() != artifact.tier_1_targets()
    234             {
    235                 return Err(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract);
    236             }
    237         }
    238         Ok(Self { contract })
    239     }
    240 
    241     pub fn contract(&self) -> &RadrootsRuntimeDistributionContract {
    242         &self.contract
    243     }
    244 
    245     pub fn service_target(
    246         &self,
    247         service_id: &ServiceId,
    248     ) -> Result<&HardenedServiceTarget, RadrootsRuntimeDistributionError> {
    249         self.contract
    250             .service_targets
    251             .get(service_id)
    252             .ok_or(RadrootsRuntimeDistributionError::UnsupportedService)
    253     }
    254 
    255     pub fn resolve_service_target(
    256         &self,
    257         request: &ServiceTargetRequest<'_>,
    258     ) -> Result<ResolvedServiceTarget, RadrootsRuntimeDistributionError> {
    259         let service = self.service_target(request.service_id)?;
    260         let target = ServiceTier1Target::parse(request.target_id)
    261             .filter(|target| service.tier_1_targets().contains(target))
    262             .ok_or(RadrootsRuntimeDistributionError::UnsupportedServiceTarget)?;
    263         Ok(ResolvedServiceTarget {
    264             service_id: request.service_id.clone(),
    265             target,
    266         })
    267     }
    268 
    269     pub fn service_artifact(
    270         &self,
    271         service_id: &ServiceId,
    272     ) -> Result<&HardenedServiceArtifact, RadrootsRuntimeDistributionError> {
    273         self.contract
    274             .service_artifacts
    275             .get(service_id)
    276             .ok_or(RadrootsRuntimeDistributionError::UnsupportedService)
    277     }
    278 
    279     pub fn resolve_service_artifact(
    280         &self,
    281         request: &ServiceArtifactRequest<'_>,
    282     ) -> Result<ResolvedServiceArtifact, RadrootsRuntimeDistributionError> {
    283         let artifact = self.service_artifact(request.service_id)?;
    284         let target = ServiceTier1Target::parse(request.target_id)
    285             .filter(|target| artifact.tier_1_targets().contains(target))
    286             .ok_or(RadrootsRuntimeDistributionError::UnsupportedServiceTarget)?;
    287         let service = artifact.service_id().as_str();
    288         let version = artifact.version();
    289         let binary = artifact.binary_name();
    290         Ok(ResolvedServiceArtifact {
    291             service_id: request.service_id.clone(),
    292             target,
    293             version: version.to_owned(),
    294             package_name: artifact.package_name().to_owned(),
    295             binary_name: binary.to_owned(),
    296             channel: artifact.channel().as_str().to_owned(),
    297             binary_archive_name: artifact.binary_archive_name().to_owned(),
    298             binary_archive_format: "tar.gz",
    299             binary_archive_member: format!("{service}-{version}-{}/{binary}", target.as_str()),
    300             artifact_manifest_name: artifact.artifact_manifest_name().to_owned(),
    301             checksums_name: artifact.checksums_name().to_owned(),
    302             checksum_algorithm: "sha256",
    303             checksum_format: "sha256_lower_hex_two_spaces_path_lf_sorted_by_path",
    304             release_contract_sha256: artifact.release_contract_sha256(),
    305             source_lock_sha256: artifact.source_lock_sha256(),
    306             output_inventory: artifact.output_inventory().to_vec(),
    307         })
    308     }
    309 
    310     pub fn resolve_artifact(
    311         &self,
    312         request: &RuntimeArtifactRequest<'_>,
    313     ) -> Result<ResolvedRuntimeArtifact, RadrootsRuntimeDistributionError> {
    314         let runtime = self
    315             .contract
    316             .runtime
    317             .iter()
    318             .find(|runtime| runtime.id == request.runtime_id)
    319             .ok_or(RadrootsRuntimeDistributionError::UnknownRuntime)?;
    320 
    321         if !runtime.human_installable {
    322             return Err(RadrootsRuntimeDistributionError::RuntimeNotInstallable);
    323         }
    324 
    325         let channel = request.channel.unwrap_or(runtime.default_channel.as_str());
    326         self.ensure_channel_is_active(channel)?;
    327 
    328         let target_set_id = runtime
    329             .target_set
    330             .as_ref()
    331             .ok_or(RadrootsRuntimeDistributionError::MissingTargetSet)?;
    332 
    333         let adapter = self
    334             .contract
    335             .artifact_adapters
    336             .get(&runtime.artifact_adapter)
    337             .ok_or(RadrootsRuntimeDistributionError::UnknownArtifactAdapter)?;
    338 
    339         let (target_id, target) =
    340             self.select_target(runtime, target_set_id, request.os, request.arch)?;
    341         let archive_format_id =
    342             self.resolve_archive_format_id(runtime, target_id, target, adapter)?;
    343         let archive_format = self
    344             .contract
    345             .archive_formats
    346             .get(&normalized_contract_key(archive_format_id))
    347             .ok_or(RadrootsRuntimeDistributionError::UnknownArchiveFormat)?;
    348 
    349         let artifact_stem = format!("{}-{}-{}", runtime.release_unit, request.version, target_id);
    350         let artifact_file_name = format!("{artifact_stem}{}", archive_format.extension);
    351 
    352         Ok(ResolvedRuntimeArtifact {
    353             runtime_id: runtime.id.clone(),
    354             release_unit: runtime.release_unit.clone(),
    355             package_name: runtime.package_name.clone(),
    356             binary_name: runtime.binary_name.clone(),
    357             artifact_adapter: runtime.artifact_adapter.clone(),
    358             channel: channel.to_string(),
    359             version: request.version.to_string(),
    360             target_id: target_id.to_string(),
    361             os: request.os.to_string(),
    362             arch: request.arch.to_string(),
    363             archive_format: archive_format_id.to_string(),
    364             archive_extension: archive_format.extension.clone(),
    365             artifact_stem,
    366             artifact_file_name,
    367         })
    368     }
    369 
    370     fn ensure_channel_is_active(
    371         &self,
    372         channel: &str,
    373     ) -> Result<(), RadrootsRuntimeDistributionError> {
    374         if !self
    375             .contract
    376             .channels
    377             .defined
    378             .iter()
    379             .any(|entry| entry == channel)
    380         {
    381             return Err(RadrootsRuntimeDistributionError::UnknownChannel);
    382         }
    383         if !self
    384             .contract
    385             .channels
    386             .active
    387             .iter()
    388             .any(|entry| entry == channel)
    389         {
    390             return Err(RadrootsRuntimeDistributionError::InactiveChannel);
    391         }
    392         Ok(())
    393     }
    394 
    395     fn select_target<'a>(
    396         &'a self,
    397         _runtime: &RuntimeDistributionEntry,
    398         target_set_id: &str,
    399         os: &str,
    400         arch: &str,
    401     ) -> Result<(&'a str, &'a TargetSpec), RadrootsRuntimeDistributionError> {
    402         let target_set = self
    403             .contract
    404             .target_sets
    405             .get(target_set_id)
    406             .ok_or(RadrootsRuntimeDistributionError::UnsupportedPlatform)?;
    407 
    408         let mut found_match = None;
    409         for target_id in &target_set.targets {
    410             let target = self
    411                 .contract
    412                 .targets
    413                 .get(target_id)
    414                 .ok_or(RadrootsRuntimeDistributionError::UnknownTarget)?;
    415 
    416             if target.os == os && target.arch == arch {
    417                 found_match = Some((target_id.as_str(), target));
    418                 break;
    419             }
    420         }
    421 
    422         found_match.ok_or(RadrootsRuntimeDistributionError::UnsupportedPlatform)
    423     }
    424 
    425     fn resolve_archive_format_id<'a>(
    426         &self,
    427         _runtime: &RuntimeDistributionEntry,
    428         _target_id: &'a str,
    429         target: &'a TargetSpec,
    430         adapter: &'a ArtifactAdapter,
    431     ) -> Result<&'a str, RadrootsRuntimeDistributionError> {
    432         if let Some(format) = target.archive_format.as_deref() {
    433             return Ok(format);
    434         }
    435 
    436         if adapter.supported_archive_formats.len() == 1 {
    437             return Ok(adapter.supported_archive_formats[0].as_str());
    438         }
    439 
    440         Err(RadrootsRuntimeDistributionError::MissingArchiveFormat)
    441     }
    442 }
    443 
    444 fn normalized_contract_key(value: &str) -> String {
    445     value.replace('.', "_")
    446 }