//! Cloud config bundle domain model and shared in-memory loader. //! //! The backend bundle groups cloud-delivered config and requirements fragments //! by source bucket. `CloudConfigBundleLayers` converts those raw buckets into //! layer entries while preserving each bucket's insertion semantics. use crate::CloudConfigFragment; use crate::ConfigLayerEntry; use crate::RequirementSource; use crate::RequirementsLayerEntry; use crate::cloud_config_layers::CloudConfigLayerError; use crate::cloud_config_layers::cloud_config_layers_from_fragments_strict; use crate::cloud_config_layers_from_fragments; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use futures::future::FutureExt; use futures::future::Shared; use serde::Deserialize; use serde::Serialize; use std::fmt; use std::future::Future; use thiserror::Error; #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudConfigBundle { pub config_toml: CloudConfigTomlBundle, pub requirements_toml: CloudRequirementsTomlBundle, } impl CloudConfigBundle { pub fn is_empty(&self) -> bool { let CloudConfigBundle { config_toml, requirements_toml, } = self; let CloudConfigTomlBundle { enterprise_managed: config_enterprise_managed, } = config_toml; let CloudRequirementsTomlBundle { enterprise_managed: requirements_enterprise_managed, } = requirements_toml; config_enterprise_managed.is_empty() && requirements_enterprise_managed.is_empty() } } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudConfigTomlBundle { pub enterprise_managed: Vec, } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudRequirementsTomlBundle { pub enterprise_managed: Vec, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudRequirementsFragment { pub id: String, pub name: String, pub contents: String, } /// Cloud config bundle converted into semantic layer buckets. /// /// This is not a final config stack. Callers still decide where each bucket is /// inserted relative to local/system/user layers. #[derive(Clone, Debug)] pub struct CloudConfigBundleLayers { /// Enterprise-managed config layers in `ConfigLayerStack` order. pub enterprise_managed_config: Vec, /// Enterprise-managed requirements layers in requirements layer merge order. pub enterprise_managed_requirements: Vec, } impl CloudConfigBundleLayers { pub fn from_bundle( bundle: CloudConfigBundle, base_dir: &AbsolutePathBuf, ) -> Result { Self::from_bundle_impl(bundle, base_dir, /*strict_config*/ false) } pub fn from_bundle_strict_config( bundle: CloudConfigBundle, base_dir: &AbsolutePathBuf, ) -> Result { Self::from_bundle_impl(bundle, base_dir, /*strict_config*/ true) } fn from_bundle_impl( bundle: CloudConfigBundle, base_dir: &AbsolutePathBuf, strict_config: bool, ) -> Result { // Keep this destructuring exhaustive so adding a new bundle bucket forces // an explicit choice about how it becomes layer data. let CloudConfigBundle { config_toml: CloudConfigTomlBundle { enterprise_managed: config_enterprise_managed, }, requirements_toml: CloudRequirementsTomlBundle { enterprise_managed: requirements_enterprise_managed, }, } = bundle; let enterprise_managed_config = if strict_config { cloud_config_layers_from_fragments_strict(config_enterprise_managed, base_dir)? } else { cloud_config_layers_from_fragments(config_enterprise_managed, base_dir)? }; let mut enterprise_managed_requirements = requirements_enterprise_managed .into_iter() .map(|fragment| { RequirementsLayerEntry::from_toml( RequirementSource::EnterpriseManaged { id: fragment.id, name: fragment.name, }, fragment.contents, ) .with_base_dir(base_dir.clone()) }) .collect::>(); // Bundle fragments arrive highest-priority first, while requirements // layers are merged lowest-priority to highest-priority. enterprise_managed_requirements.reverse(); Ok(Self { enterprise_managed_config, enterprise_managed_requirements, }) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CloudConfigBundleLoadErrorCode { Auth, Timeout, RequestFailed, InvalidBundle, Internal, } #[derive(Clone, Debug, Eq, Error, PartialEq)] #[error("{message}")] pub struct CloudConfigBundleLoadError { code: CloudConfigBundleLoadErrorCode, message: String, status_code: Option, } impl CloudConfigBundleLoadError { pub fn new( code: CloudConfigBundleLoadErrorCode, status_code: Option, message: impl Into, ) -> Self { Self { code, message: message.into(), status_code, } } pub fn code(&self) -> CloudConfigBundleLoadErrorCode { self.code } pub fn status_code(&self) -> Option { self.status_code } } #[derive(Clone)] pub struct CloudConfigBundleLoader { fut: Shared, CloudConfigBundleLoadError>>>, } impl CloudConfigBundleLoader { pub fn new(fut: F) -> Self where F: Future, CloudConfigBundleLoadError>> + Send + 'static, { Self { fut: fut.boxed().shared(), } } pub async fn get(&self) -> Result, CloudConfigBundleLoadError> { self.fut.clone().await } } impl fmt::Debug for CloudConfigBundleLoader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("CloudConfigBundleLoader").finish() } } impl Default for CloudConfigBundleLoader { fn default() -> Self { Self::new(async { Ok(None) }) } } #[cfg(test)] #[path = "cloud_config_bundle_tests.rs"] mod tests;