File size: 5,010 Bytes
bb5a19b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | //! Conversion from cloud-delivered config TOML fragments into config stack layers.
//!
//! Backend fragments arrive in backend priority order. This module parses each
//! fragment, resolves relative path fields against the cloud config base
//! directory, and returns layers in `ConfigLayerStack` order.
use crate::ConfigLayerEntry;
use crate::ConfigLayerSource;
use crate::TomlValue;
use crate::config_toml::ConfigToml;
use crate::loader::resolve_relative_paths_in_config_toml;
use crate::strict_config::config_error_from_ignored_toml_value_fields_for_source_name;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::io;
use thiserror::Error;
/// Config fragment delivered by the cloud config bundle.
///
/// The bundle orders fragments from highest precedence to lowest precedence.
/// This module returns config layers in stack order, so callers can append the
/// result between system and user config without re-sorting.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CloudConfigFragment {
pub id: String,
pub name: String,
pub contents: String,
}
impl CloudConfigFragment {
fn source_ref(&self) -> CloudConfigFragmentSource {
CloudConfigFragmentSource {
id: self.id.clone(),
name: self.name.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CloudConfigFragmentSource {
pub id: String,
pub name: String,
}
impl fmt::Display for CloudConfigFragmentSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name, self.id)
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum CloudConfigLayerError {
#[error("failed to parse cloud config fragment {fragment}: {message}")]
Parse {
fragment: CloudConfigFragmentSource,
message: String,
},
#[error("invalid cloud config fragment {fragment}: {message}")]
Invalid {
fragment: CloudConfigFragmentSource,
message: String,
},
}
pub fn cloud_config_layers_from_fragments(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ false)
}
pub(crate) fn cloud_config_layers_from_fragments_strict(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ true)
}
fn cloud_config_layers_from_fragments_impl(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
strict_config: bool,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
let mut layers = Vec::new();
for fragment in fragments {
let source_ref = fragment.source_ref();
let raw_toml = fragment.contents;
let value: TomlValue =
toml::from_str(&raw_toml).map_err(|err| CloudConfigLayerError::Parse {
fragment: source_ref.clone(),
message: err.to_string(),
})?;
if strict_config {
validate_fragment_strictly(&source_ref, &raw_toml, &value, base_dir)?;
}
let resolved =
resolve_relative_paths_in_config_toml(value, base_dir.as_path()).map_err(|err| {
CloudConfigLayerError::Invalid {
fragment: source_ref.clone(),
message: err.to_string(),
}
})?;
layers.push(ConfigLayerEntry::new_with_raw_toml(
ConfigLayerSource::EnterpriseManaged {
id: fragment.id,
name: fragment.name,
},
resolved,
raw_toml,
base_dir.clone(),
));
}
// Bundle fragments arrive highest-priority first, while ConfigLayerStack
// folds lowest-priority to highest-priority.
layers.reverse();
Ok(layers)
}
fn validate_fragment_strictly(
source_ref: &CloudConfigFragmentSource,
raw_toml: &str,
value: &TomlValue,
base_dir: &AbsolutePathBuf,
) -> Result<(), CloudConfigLayerError> {
let _guard = AbsolutePathBufGuard::new(base_dir.as_path());
if let Some(config_error) = config_error_from_ignored_toml_value_fields_for_source_name::<
ConfigToml,
>(&source_ref.to_string(), raw_toml, value.clone())
{
return Err(CloudConfigLayerError::Invalid {
fragment: source_ref.clone(),
message: config_error.message,
});
}
Ok(())
}
impl From<CloudConfigLayerError> for io::Error {
fn from(error: CloudConfigLayerError) -> Self {
io::Error::new(io::ErrorKind::InvalidData, error)
}
}
#[cfg(test)]
#[path = "cloud_config_layers_tests.rs"]
mod tests;
|