mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
[cloud-config] Add managed-layer wire and cache support
This commit is contained in:
@@ -15,6 +15,7 @@ pub use types::ConfigBundleResponse;
|
||||
pub use types::ConsumeRateLimitResetCreditCode;
|
||||
pub use types::ConsumeRateLimitResetCreditResponse;
|
||||
pub use types::DeliveredConfigToml;
|
||||
pub use types::DeliveredManagedLayers;
|
||||
pub use types::DeliveredRequirementsToml;
|
||||
pub use types::DeliveredTomlFragment;
|
||||
pub use types::PaginatedListTaskListItem;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use codex_backend_openapi_models::models::ConfigBundleResponse;
|
||||
pub use codex_backend_openapi_models::models::CreditStatusDetails;
|
||||
pub use codex_backend_openapi_models::models::DeliveredConfigToml;
|
||||
pub use codex_backend_openapi_models::models::DeliveredManagedLayers;
|
||||
pub use codex_backend_openapi_models::models::DeliveredRequirementsToml;
|
||||
pub use codex_backend_openapi_models::models::DeliveredTomlFragment;
|
||||
pub use codex_backend_openapi_models::models::PaginatedListTaskListItem;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_backend_client::ConfigBundleResponse;
|
||||
use codex_backend_client::DeliveredManagedLayers;
|
||||
use codex_backend_client::DeliveredTomlFragment;
|
||||
use codex_config::CloudConfigBundle;
|
||||
use codex_config::CloudConfigFragment;
|
||||
use codex_config::CloudConfigTomlBundle;
|
||||
use codex_config::CloudConfigTomlManagedLayers;
|
||||
use codex_config::CloudRequirementsFragment;
|
||||
use codex_config::CloudRequirementsTomlBundle;
|
||||
use codex_config::CloudRequirementsTomlManagedLayers;
|
||||
use codex_login::CodexAuth;
|
||||
use std::future::Future;
|
||||
|
||||
@@ -92,16 +95,30 @@ pub(crate) fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfi
|
||||
.config_toml
|
||||
.flatten()
|
||||
.map(|config_toml| *config_toml)
|
||||
.and_then(|config_toml| config_toml.enterprise_managed.flatten())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(config_fragment_from_delivered)
|
||||
.collect();
|
||||
.unwrap_or_default();
|
||||
let requirements_toml = response
|
||||
.requirements_toml
|
||||
.flatten()
|
||||
.map(|requirements_toml| *requirements_toml)
|
||||
.and_then(|requirements_toml| requirements_toml.enterprise_managed.flatten())
|
||||
.unwrap_or_default();
|
||||
let (config_baseline, config_system_overlay) = managed_fragments_from_delivered(
|
||||
config_toml.managed_layers,
|
||||
config_fragment_from_delivered,
|
||||
);
|
||||
let (requirements_baseline, requirements_system_overlay) = managed_fragments_from_delivered(
|
||||
requirements_toml.managed_layers,
|
||||
requirements_fragment_from_delivered,
|
||||
);
|
||||
let config_enterprise_managed = config_toml
|
||||
.enterprise_managed
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(config_fragment_from_delivered)
|
||||
.collect();
|
||||
let requirements_enterprise_managed = requirements_toml
|
||||
.enterprise_managed
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(requirements_fragment_from_delivered)
|
||||
@@ -109,14 +126,41 @@ pub(crate) fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfi
|
||||
|
||||
CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle {
|
||||
enterprise_managed: config_toml,
|
||||
enterprise_managed: config_enterprise_managed,
|
||||
managed_layers: CloudConfigTomlManagedLayers {
|
||||
baseline: config_baseline,
|
||||
system_overlay: config_system_overlay,
|
||||
},
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: requirements_toml,
|
||||
enterprise_managed: requirements_enterprise_managed,
|
||||
managed_layers: CloudRequirementsTomlManagedLayers {
|
||||
baseline: requirements_baseline,
|
||||
system_overlay: requirements_system_overlay,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn managed_fragments_from_delivered<T>(
|
||||
managed_layers: Option<Option<Box<DeliveredManagedLayers>>>,
|
||||
convert: fn(DeliveredTomlFragment) -> T,
|
||||
) -> (Option<Vec<T>>, Option<Vec<T>>) {
|
||||
let managed_layers = managed_layers
|
||||
.flatten()
|
||||
.map(|managed_layers| *managed_layers)
|
||||
.unwrap_or_default();
|
||||
let baseline = managed_layers
|
||||
.baseline
|
||||
.flatten()
|
||||
.map(|fragments| fragments.into_iter().map(convert).collect());
|
||||
let system_overlay = managed_layers
|
||||
.system_overlay
|
||||
.flatten()
|
||||
.map(|fragments| fragments.into_iter().map(convert).collect());
|
||||
(baseline, system_overlay)
|
||||
}
|
||||
|
||||
fn config_fragment_from_delivered(fragment: DeliveredTomlFragment) -> CloudConfigFragment {
|
||||
CloudConfigFragment {
|
||||
id: fragment.id,
|
||||
|
||||
@@ -27,6 +27,9 @@ const CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY: &[u8] =
|
||||
b"codex-cloud-config-bundle-cache-v1-6160ae70-bcfd-4ca8-a99b-40f73b3b072e";
|
||||
const CLOUD_CONFIG_BUNDLE_CACHE_READ_HMAC_KEYS: &[&[u8]] =
|
||||
&[CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY];
|
||||
const MANAGED_LAYERS_CACHE_WRITE_HMAC_KEY: &[u8] =
|
||||
b"codex-cloud-config-bundle-cache-v1-managed-layers-8fe40af0-ec6d-4740-900d-d4bdd771ea2f";
|
||||
const MANAGED_LAYERS_CACHE_READ_HMAC_KEYS: &[&[u8]] = &[MANAGED_LAYERS_CACHE_WRITE_HMAC_KEY];
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
@@ -71,7 +74,7 @@ impl CloudConfigBundleCache {
|
||||
return Err(CacheLoadStatus::CacheParseFailed(err.to_string()));
|
||||
}
|
||||
};
|
||||
let payload_bytes = match cache_payload_bytes(&cache_file.signed_payload) {
|
||||
let legacy_payload_bytes = match legacy_cache_payload_bytes(&cache_file.signed_payload) {
|
||||
Some(payload_bytes) => payload_bytes,
|
||||
None => {
|
||||
return Err(CacheLoadStatus::CacheParseFailed(
|
||||
@@ -79,9 +82,26 @@ impl CloudConfigBundleCache {
|
||||
));
|
||||
}
|
||||
};
|
||||
if !verify_cache_signature(&payload_bytes, &cache_file.signature) {
|
||||
if !verify_cache_signature(&legacy_payload_bytes, &cache_file.signature) {
|
||||
return Err(CacheLoadStatus::CacheSignatureInvalid);
|
||||
}
|
||||
match cache_file.managed_layers_signature.as_deref() {
|
||||
Some(signature) => {
|
||||
let payload_bytes =
|
||||
cache_payload_bytes(&cache_file.signed_payload).ok_or_else(|| {
|
||||
CacheLoadStatus::CacheParseFailed(
|
||||
"failed to serialize cache payload".to_string(),
|
||||
)
|
||||
})?;
|
||||
if !verify_managed_layers_cache_signature(&payload_bytes, signature) {
|
||||
return Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid);
|
||||
}
|
||||
}
|
||||
None if cache_file.signed_payload.bundle.has_managed_layer_buckets() => {
|
||||
return Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if cache_file.signed_payload.version != CLOUD_CONFIG_BUNDLE_CACHE_VERSION {
|
||||
return Err(CacheLoadStatus::CacheVersionUnsupported(
|
||||
cache_file.signed_payload.version,
|
||||
@@ -116,6 +136,7 @@ impl CloudConfigBundleCache {
|
||||
CacheLoadStatus::CacheReadFailed(_)
|
||||
| CacheLoadStatus::CacheParseFailed(_)
|
||||
| CacheLoadStatus::CacheSignatureInvalid
|
||||
| CacheLoadStatus::CacheManagedLayersSignatureInvalid
|
||||
);
|
||||
|
||||
if warn {
|
||||
@@ -146,10 +167,17 @@ impl CloudConfigBundleCache {
|
||||
account_id,
|
||||
bundle,
|
||||
};
|
||||
let legacy_payload_bytes =
|
||||
legacy_cache_payload_bytes(&signed_payload).ok_or(CloudConfigBundleCacheError)?;
|
||||
let payload_bytes =
|
||||
cache_payload_bytes(&signed_payload).ok_or(CloudConfigBundleCacheError)?;
|
||||
let serialized = serde_json::to_vec_pretty(&CloudConfigBundleCacheFile {
|
||||
signature: sign_cache_payload(&payload_bytes).ok_or(CloudConfigBundleCacheError)?,
|
||||
signature: sign_cache_payload(&legacy_payload_bytes)
|
||||
.ok_or(CloudConfigBundleCacheError)?,
|
||||
managed_layers_signature: Some(
|
||||
sign_managed_layers_cache_payload(&payload_bytes)
|
||||
.ok_or(CloudConfigBundleCacheError)?,
|
||||
),
|
||||
signed_payload,
|
||||
})
|
||||
.map_err(|_| CloudConfigBundleCacheError)?;
|
||||
@@ -179,6 +207,8 @@ pub(super) enum CacheLoadStatus {
|
||||
CacheParseFailed(String),
|
||||
#[error("Cloud config bundle cache failed signature verification.")]
|
||||
CacheSignatureInvalid,
|
||||
#[error("Cloud config bundle cache failed managed-layers signature verification.")]
|
||||
CacheManagedLayersSignatureInvalid,
|
||||
#[error("Ignoring cloud config bundle cache because cached identity is incomplete.")]
|
||||
CacheIdentityIncomplete,
|
||||
#[error("Ignoring cloud config bundle cache for different auth identity.")]
|
||||
@@ -199,6 +229,8 @@ pub(super) struct CloudConfigBundleCacheError;
|
||||
pub(super) struct CloudConfigBundleCacheFile {
|
||||
pub(super) signed_payload: CloudConfigBundleCacheSignedPayload,
|
||||
pub(super) signature: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(super) managed_layers_signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
@@ -217,11 +249,21 @@ pub(super) fn cache_payload_bytes(
|
||||
serde_json::to_vec(&payload).ok()
|
||||
}
|
||||
|
||||
pub(super) fn legacy_cache_payload_bytes(
|
||||
payload: &CloudConfigBundleCacheSignedPayload,
|
||||
) -> Option<Vec<u8>> {
|
||||
let mut legacy_payload = payload.clone();
|
||||
legacy_payload.bundle.config_toml.managed_layers = Default::default();
|
||||
legacy_payload.bundle.requirements_toml.managed_layers = Default::default();
|
||||
cache_payload_bytes(&legacy_payload)
|
||||
}
|
||||
|
||||
pub(super) fn sign_cache_payload(payload_bytes: &[u8]) -> Option<String> {
|
||||
let mut mac = HmacSha256::new_from_slice(CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY).ok()?;
|
||||
mac.update(payload_bytes);
|
||||
let signature = mac.finalize().into_bytes();
|
||||
Some(BASE64_STANDARD.encode(signature))
|
||||
sign_cache_payload_with_key(payload_bytes, CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY)
|
||||
}
|
||||
|
||||
pub(super) fn sign_managed_layers_cache_payload(payload_bytes: &[u8]) -> Option<String> {
|
||||
sign_cache_payload_with_key(payload_bytes, MANAGED_LAYERS_CACHE_WRITE_HMAC_KEY)
|
||||
}
|
||||
|
||||
pub(super) fn verify_cache_signature(payload_bytes: &[u8], signature: &str) -> bool {
|
||||
@@ -235,6 +277,24 @@ pub(super) fn verify_cache_signature(payload_bytes: &[u8], signature: &str) -> b
|
||||
.any(|key| verify_cache_signature_with_key(payload_bytes, &signature_bytes, key))
|
||||
}
|
||||
|
||||
pub(super) fn verify_managed_layers_cache_signature(payload_bytes: &[u8], signature: &str) -> bool {
|
||||
let signature_bytes = match BASE64_STANDARD.decode(signature) {
|
||||
Ok(signature_bytes) => signature_bytes,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
MANAGED_LAYERS_CACHE_READ_HMAC_KEYS
|
||||
.iter()
|
||||
.any(|key| verify_cache_signature_with_key(payload_bytes, &signature_bytes, key))
|
||||
}
|
||||
|
||||
fn sign_cache_payload_with_key(payload_bytes: &[u8], key: &[u8]) -> Option<String> {
|
||||
let mut mac = HmacSha256::new_from_slice(key).ok()?;
|
||||
mac.update(payload_bytes);
|
||||
let signature = mac.finalize().into_bytes();
|
||||
Some(BASE64_STANDARD.encode(signature))
|
||||
}
|
||||
|
||||
fn verify_cache_signature_with_key(
|
||||
payload_bytes: &[u8],
|
||||
signature_bytes: &[u8],
|
||||
|
||||
@@ -2,8 +2,10 @@ use super::*;
|
||||
use codex_config::AbsolutePathBuf;
|
||||
use codex_config::CloudConfigFragment;
|
||||
use codex_config::CloudConfigTomlBundle;
|
||||
use codex_config::CloudConfigTomlManagedLayers;
|
||||
use codex_config::CloudRequirementsFragment;
|
||||
use codex_config::CloudRequirementsTomlBundle;
|
||||
use codex_config::CloudRequirementsTomlManagedLayers;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
@@ -16,6 +18,7 @@ fn test_bundle() -> CloudConfigBundle {
|
||||
name: "Base config".to_string(),
|
||||
contents: "model = \"gpt-5\"".to_string(),
|
||||
}],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: vec![CloudRequirementsFragment {
|
||||
@@ -23,6 +26,7 @@ fn test_bundle() -> CloudConfigBundle {
|
||||
name: "Base requirements".to_string(),
|
||||
contents: "allowed_approval_policies = [\"never\"]".to_string(),
|
||||
}],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -31,8 +35,24 @@ fn signed_cache_file(
|
||||
signed_payload: CloudConfigBundleCacheSignedPayload,
|
||||
) -> CloudConfigBundleCacheFile {
|
||||
let payload_bytes = cache_payload_bytes(&signed_payload).expect("payload bytes");
|
||||
let legacy_payload_bytes =
|
||||
legacy_cache_payload_bytes(&signed_payload).expect("legacy payload bytes");
|
||||
CloudConfigBundleCacheFile {
|
||||
signature: sign_cache_payload(&legacy_payload_bytes).expect("signature"),
|
||||
managed_layers_signature: Some(
|
||||
sign_managed_layers_cache_payload(&payload_bytes).expect("managed signature"),
|
||||
),
|
||||
signed_payload,
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_signed_cache_file(
|
||||
signed_payload: CloudConfigBundleCacheSignedPayload,
|
||||
) -> CloudConfigBundleCacheFile {
|
||||
let payload_bytes = legacy_cache_payload_bytes(&signed_payload).expect("legacy payload bytes");
|
||||
CloudConfigBundleCacheFile {
|
||||
signature: sign_cache_payload(&payload_bytes).expect("signature"),
|
||||
managed_layers_signature: None,
|
||||
signed_payload,
|
||||
}
|
||||
}
|
||||
@@ -49,6 +69,27 @@ fn valid_signed_payload() -> CloudConfigBundleCacheSignedPayload {
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_managed_signed_payload() -> CloudConfigBundleCacheSignedPayload {
|
||||
let mut payload = valid_signed_payload();
|
||||
payload.bundle.config_toml.managed_layers = CloudConfigTomlManagedLayers {
|
||||
baseline: Some(vec![CloudConfigFragment {
|
||||
id: "cfg_baseline".to_string(),
|
||||
name: "Config baseline".to_string(),
|
||||
contents: "model = \"baseline\"".to_string(),
|
||||
}]),
|
||||
system_overlay: Some(Vec::new()),
|
||||
};
|
||||
payload.bundle.requirements_toml.managed_layers = CloudRequirementsTomlManagedLayers {
|
||||
baseline: None,
|
||||
system_overlay: Some(vec![CloudRequirementsFragment {
|
||||
id: "req_overlay".to_string(),
|
||||
name: "Requirements overlay".to_string(),
|
||||
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
|
||||
}]),
|
||||
};
|
||||
payload
|
||||
}
|
||||
|
||||
fn write_cache_file(cache: &CloudConfigBundleCache, cache_file: &CloudConfigBundleCacheFile) {
|
||||
std::fs::write(
|
||||
cache.path(),
|
||||
@@ -61,11 +102,42 @@ fn create_test_cache(codex_home: &Path) -> CloudConfigBundleCache {
|
||||
CloudConfigBundleCache::new(AbsolutePathBuf::resolve_path_against_base(codex_home, "/"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_signature_covers_only_the_legacy_projection() {
|
||||
let payload = valid_managed_signed_payload();
|
||||
let cache_file = signed_cache_file(payload.clone());
|
||||
let legacy_payload_bytes = legacy_cache_payload_bytes(&payload).expect("legacy payload bytes");
|
||||
let payload_bytes = cache_payload_bytes(&payload).expect("payload bytes");
|
||||
let legacy_payload: serde_json::Value =
|
||||
serde_json::from_slice(&legacy_payload_bytes).expect("legacy payload");
|
||||
let full_payload: serde_json::Value =
|
||||
serde_json::from_slice(&payload_bytes).expect("full payload");
|
||||
|
||||
assert!(
|
||||
legacy_payload
|
||||
.pointer("/bundle/config_toml/managed_layers")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
full_payload
|
||||
.pointer("/bundle/config_toml/managed_layers")
|
||||
.is_some()
|
||||
);
|
||||
assert!(verify_cache_signature(
|
||||
&legacy_payload_bytes,
|
||||
&cache_file.signature
|
||||
));
|
||||
assert!(!verify_cache_signature(
|
||||
&payload_bytes,
|
||||
&cache_file.signature
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_writes_signed_payload_and_loads_for_matching_identity() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache = create_test_cache(codex_home.path());
|
||||
let bundle = test_bundle();
|
||||
let bundle = valid_managed_signed_payload().bundle;
|
||||
|
||||
cache
|
||||
.save(
|
||||
@@ -102,6 +174,45 @@ async fn save_writes_signed_payload_and_loads_for_matching_identity() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_accepts_legacy_cache_without_managed_data() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache = create_test_cache(codex_home.path());
|
||||
let cache_file = legacy_signed_cache_file(valid_signed_payload());
|
||||
write_cache_file(&cache, &cache_file);
|
||||
|
||||
assert_eq!(
|
||||
cache.load(Some("user-12345"), Some("account-12345")).await,
|
||||
Ok(cache_file.signed_payload)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_requires_full_signature_for_managed_data() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache = create_test_cache(codex_home.path());
|
||||
let cache_file = legacy_signed_cache_file(valid_managed_signed_payload());
|
||||
write_cache_file(&cache, &cache_file);
|
||||
|
||||
assert_eq!(
|
||||
cache.load(Some("user-12345"), Some("account-12345")).await,
|
||||
Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_accepts_managed_data_with_both_signatures() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache = create_test_cache(codex_home.path());
|
||||
let cache_file = signed_cache_file(valid_managed_signed_payload());
|
||||
write_cache_file(&cache, &cache_file);
|
||||
|
||||
assert_eq!(
|
||||
cache.load(Some("user-12345"), Some("account-12345")).await,
|
||||
Ok(cache_file.signed_payload)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_rejects_missing_request_identity_before_reading_cache_file() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
@@ -155,6 +266,44 @@ async fn load_rejects_tampered_payload() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_rejects_tampered_managed_data() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache = create_test_cache(codex_home.path());
|
||||
let mut cache_file = signed_cache_file(valid_managed_signed_payload());
|
||||
cache_file
|
||||
.signed_payload
|
||||
.bundle
|
||||
.requirements_toml
|
||||
.managed_layers
|
||||
.system_overlay
|
||||
.as_mut()
|
||||
.expect("system overlay")
|
||||
.first_mut()
|
||||
.expect("requirements fragment")
|
||||
.contents = "allowed_approval_policies = [\"never\"]".to_string();
|
||||
write_cache_file(&cache, &cache_file);
|
||||
|
||||
assert_eq!(
|
||||
cache.load(Some("user-12345"), Some("account-12345")).await,
|
||||
Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_rejects_tampered_managed_layers_signature() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache = create_test_cache(codex_home.path());
|
||||
let mut cache_file = signed_cache_file(valid_managed_signed_payload());
|
||||
cache_file.managed_layers_signature = Some("tampered".to_string());
|
||||
write_cache_file(&cache, &cache_file);
|
||||
|
||||
assert_eq!(
|
||||
cache.load(Some("user-12345"), Some("account-12345")).await,
|
||||
Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_rejects_cache_for_incomplete_or_different_identity() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
|
||||
@@ -66,9 +66,26 @@ pub(crate) fn bundle_shape_tag(bundle: Option<&CloudConfigBundle>) -> String {
|
||||
if !bundle.config_toml.enterprise_managed.is_empty() {
|
||||
sources.push("enterprise_config");
|
||||
}
|
||||
if bundle.config_toml.managed_layers.baseline.is_some() {
|
||||
sources.push("baseline_config");
|
||||
}
|
||||
if bundle.config_toml.managed_layers.system_overlay.is_some() {
|
||||
sources.push("system_overlay_config");
|
||||
}
|
||||
if !bundle.requirements_toml.enterprise_managed.is_empty() {
|
||||
sources.push("enterprise_requirements");
|
||||
}
|
||||
if bundle.requirements_toml.managed_layers.baseline.is_some() {
|
||||
sources.push("baseline_requirements");
|
||||
}
|
||||
if bundle
|
||||
.requirements_toml
|
||||
.managed_layers
|
||||
.system_overlay
|
||||
.is_some()
|
||||
{
|
||||
sources.push("system_overlay_requirements");
|
||||
}
|
||||
|
||||
if sources.is_empty() {
|
||||
"empty".to_string()
|
||||
|
||||
@@ -152,6 +152,30 @@ where
|
||||
elapsed_ms = started_at.elapsed().as_millis(),
|
||||
config_fragments = bundle.config_toml.enterprise_managed.len(),
|
||||
requirements_fragments = bundle.requirements_toml.enterprise_managed.len(),
|
||||
config_baseline_fragments = ?bundle
|
||||
.config_toml
|
||||
.managed_layers
|
||||
.baseline
|
||||
.as_ref()
|
||||
.map(Vec::len),
|
||||
config_system_overlay_fragments = ?bundle
|
||||
.config_toml
|
||||
.managed_layers
|
||||
.system_overlay
|
||||
.as_ref()
|
||||
.map(Vec::len),
|
||||
requirements_baseline_fragments = ?bundle
|
||||
.requirements_toml
|
||||
.managed_layers
|
||||
.baseline
|
||||
.as_ref()
|
||||
.map(Vec::len),
|
||||
requirements_system_overlay_fragments = ?bundle
|
||||
.requirements_toml
|
||||
.managed_layers
|
||||
.system_overlay
|
||||
.as_ref()
|
||||
.map(Vec::len),
|
||||
"Cloud config bundle load completed"
|
||||
);
|
||||
emit_load_metric("startup", "success", Some(bundle));
|
||||
|
||||
@@ -9,12 +9,15 @@ use crate::metrics::bundle_shape_tag;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use codex_backend_client::ConfigBundleResponse;
|
||||
use codex_backend_client::DeliveredManagedLayers;
|
||||
use codex_backend_client::DeliveredTomlFragment;
|
||||
use codex_config::AbsolutePathBuf;
|
||||
use codex_config::CloudConfigFragment;
|
||||
use codex_config::CloudConfigTomlBundle;
|
||||
use codex_config::CloudConfigTomlManagedLayers;
|
||||
use codex_config::CloudRequirementsFragment;
|
||||
use codex_config::CloudRequirementsTomlBundle;
|
||||
use codex_config::CloudRequirementsTomlManagedLayers;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::auth::AgentIdentityAuth;
|
||||
@@ -197,9 +200,11 @@ fn test_bundle() -> CloudConfigBundle {
|
||||
CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle {
|
||||
enterprise_managed: vec![test_config_fragment()],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: vec![test_requirements_fragment()],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -228,11 +233,29 @@ fn invalid_config_bundle() -> CloudConfigBundle {
|
||||
name: "Invalid config".to_string(),
|
||||
contents: "model = [".to_string(),
|
||||
}],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_managed_requirements_bundle() -> CloudConfigBundle {
|
||||
CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle::default(),
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: Vec::new(),
|
||||
managed_layers: CloudRequirementsTomlManagedLayers {
|
||||
baseline: None,
|
||||
system_overlay: Some(vec![CloudRequirementsFragment {
|
||||
id: "req_invalid".to_string(),
|
||||
name: "Invalid managed requirements".to_string(),
|
||||
contents: "allowed_approval_policies = [".to_string(),
|
||||
}]),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn request_error() -> BundleRequestError {
|
||||
BundleRequestError::Retryable(RetryableFailureKind::Request { status_code: None })
|
||||
}
|
||||
@@ -330,7 +353,7 @@ impl BundleClient for UnauthorizedBundleClient {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_shape_tag_describes_sorted_enterprise_sources() {
|
||||
fn bundle_shape_tag_describes_sorted_buckets() {
|
||||
assert_eq!(bundle_shape_tag(/*bundle*/ None), "none");
|
||||
assert_eq!(
|
||||
bundle_shape_tag(Some(&CloudConfigBundle::default())),
|
||||
@@ -340,6 +363,7 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
|
||||
bundle_shape_tag(Some(&CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle {
|
||||
enterprise_managed: vec![test_config_fragment()],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle::default(),
|
||||
})),
|
||||
@@ -350,6 +374,7 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
|
||||
config_toml: CloudConfigTomlBundle::default(),
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: vec![test_requirements_fragment()],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
})),
|
||||
"enterprise_requirements"
|
||||
@@ -358,12 +383,23 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
|
||||
bundle_shape_tag(Some(&CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle {
|
||||
enterprise_managed: vec![test_config_fragment()],
|
||||
managed_layers: CloudConfigTomlManagedLayers {
|
||||
baseline: Some(vec![test_config_fragment()]),
|
||||
system_overlay: Some(Vec::new()),
|
||||
},
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: vec![test_requirements_fragment()],
|
||||
managed_layers: CloudRequirementsTomlManagedLayers {
|
||||
baseline: Some(vec![test_requirements_fragment()]),
|
||||
system_overlay: Some(Vec::new()),
|
||||
},
|
||||
},
|
||||
})),
|
||||
"enterprise_config,enterprise_requirements"
|
||||
concat!(
|
||||
"baseline_config,baseline_requirements,enterprise_config,",
|
||||
"enterprise_requirements,system_overlay_config,system_overlay_requirements"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -475,30 +511,35 @@ async fn get_bundle_skips_team_like_usage_based_plan() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_bundle_rejects_invalid_remote_bundle_before_cache_write() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let fetcher = Arc::new(StaticBundleClient::new(invalid_config_bundle()));
|
||||
let service = CloudConfigBundleService::new(
|
||||
auth_manager_with_plan("business").await,
|
||||
fetcher.clone(),
|
||||
codex_home.path().to_path_buf(),
|
||||
CLOUD_CONFIG_BUNDLE_TIMEOUT,
|
||||
);
|
||||
async fn get_bundle_rejects_invalid_remote_buckets_before_cache_write() {
|
||||
for bundle in [
|
||||
invalid_config_bundle(),
|
||||
invalid_managed_requirements_bundle(),
|
||||
] {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let fetcher = Arc::new(StaticBundleClient::new(bundle));
|
||||
let service = CloudConfigBundleService::new(
|
||||
auth_manager_with_plan("business").await,
|
||||
fetcher.clone(),
|
||||
codex_home.path().to_path_buf(),
|
||||
CLOUD_CONFIG_BUNDLE_TIMEOUT,
|
||||
);
|
||||
|
||||
let err = service
|
||||
.load_startup_bundle()
|
||||
.await
|
||||
.expect_err("invalid remote bundle should fail closed");
|
||||
let err = service
|
||||
.load_startup_bundle()
|
||||
.await
|
||||
.expect_err("invalid remote bundle should fail closed");
|
||||
|
||||
assert_eq!(err.code(), CloudConfigBundleLoadErrorCode::InvalidBundle);
|
||||
assert!(err.to_string().contains("invalid cloud config bundle"));
|
||||
assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1);
|
||||
assert!(
|
||||
!codex_home
|
||||
.path()
|
||||
.join(CLOUD_CONFIG_BUNDLE_CACHE_FILENAME)
|
||||
.exists()
|
||||
);
|
||||
assert_eq!(err.code(), CloudConfigBundleLoadErrorCode::InvalidBundle);
|
||||
assert!(err.to_string().contains("invalid cloud config bundle"));
|
||||
assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1);
|
||||
assert!(
|
||||
!codex_home
|
||||
.path()
|
||||
.join(CLOUD_CONFIG_BUNDLE_CACHE_FILENAME)
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -602,6 +643,7 @@ async fn get_bundle_ignores_cache_for_different_auth_identity() {
|
||||
name: "Replacement requirements".to_string(),
|
||||
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
|
||||
}],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
};
|
||||
let fetcher = Arc::new(SequenceBundleClient::new(vec![Ok(
|
||||
@@ -931,6 +973,7 @@ async fn get_bundle_does_not_use_cache_when_auth_identity_is_incomplete() {
|
||||
name: "Replacement requirements".to_string(),
|
||||
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
|
||||
}],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
};
|
||||
let fetcher = Arc::new(SequenceBundleClient::new(vec![Ok(
|
||||
@@ -996,6 +1039,7 @@ async fn refresh_from_remote_updates_cached_bundle() {
|
||||
name: "Replacement requirements".to_string(),
|
||||
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
|
||||
}],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
};
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
@@ -1022,7 +1066,7 @@ async fn refresh_from_remote_updates_cached_bundle() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_response_conversion_preserves_fragment_order() {
|
||||
fn bundle_response_conversion_preserves_fragment_order_and_managed_presence() {
|
||||
let response = ConfigBundleResponse {
|
||||
config_toml: Some(Some(Box::new(codex_backend_client::DeliveredConfigToml {
|
||||
enterprise_managed: Some(Some(vec![
|
||||
@@ -1037,6 +1081,14 @@ fn bundle_response_conversion_preserves_fragment_order() {
|
||||
"model = \"low\"".to_string(),
|
||||
),
|
||||
])),
|
||||
managed_layers: Some(Some(Box::new(DeliveredManagedLayers {
|
||||
baseline: Some(Some(vec![DeliveredTomlFragment::new(
|
||||
"cfg_baseline".to_string(),
|
||||
"Config baseline".to_string(),
|
||||
"model = \"baseline\"".to_string(),
|
||||
)])),
|
||||
system_overlay: Some(Some(Vec::new())),
|
||||
}))),
|
||||
}))),
|
||||
requirements_toml: Some(Some(Box::new(
|
||||
codex_backend_client::DeliveredRequirementsToml {
|
||||
@@ -1045,6 +1097,14 @@ fn bundle_response_conversion_preserves_fragment_order() {
|
||||
"High requirements".to_string(),
|
||||
"allowed_approval_policies = [\"never\"]".to_string(),
|
||||
)])),
|
||||
managed_layers: Some(Some(Box::new(DeliveredManagedLayers {
|
||||
baseline: None,
|
||||
system_overlay: Some(Some(vec![DeliveredTomlFragment::new(
|
||||
"req_overlay".to_string(),
|
||||
"Requirements overlay".to_string(),
|
||||
"allowed_approval_policies = [\"on-request\"]".to_string(),
|
||||
)])),
|
||||
}))),
|
||||
},
|
||||
))),
|
||||
};
|
||||
@@ -1065,6 +1125,14 @@ fn bundle_response_conversion_preserves_fragment_order() {
|
||||
contents: "model = \"low\"".to_string(),
|
||||
},
|
||||
],
|
||||
managed_layers: CloudConfigTomlManagedLayers {
|
||||
baseline: Some(vec![CloudConfigFragment {
|
||||
id: "cfg_baseline".to_string(),
|
||||
name: "Config baseline".to_string(),
|
||||
contents: "model = \"baseline\"".to_string(),
|
||||
}]),
|
||||
system_overlay: Some(Vec::new()),
|
||||
},
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: vec![CloudRequirementsFragment {
|
||||
@@ -1072,6 +1140,14 @@ fn bundle_response_conversion_preserves_fragment_order() {
|
||||
name: "High requirements".to_string(),
|
||||
contents: "allowed_approval_policies = [\"never\"]".to_string(),
|
||||
}],
|
||||
managed_layers: CloudRequirementsTomlManagedLayers {
|
||||
baseline: None,
|
||||
system_overlay: Some(vec![CloudRequirementsFragment {
|
||||
id: "req_overlay".to_string(),
|
||||
name: "Requirements overlay".to_string(),
|
||||
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
|
||||
}]),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,17 +18,27 @@ pub(crate) fn validate_bundle(
|
||||
)
|
||||
})?;
|
||||
let CloudConfigBundleLayers {
|
||||
baseline_config: _,
|
||||
system_overlay_config: _,
|
||||
enterprise_managed_config: _,
|
||||
baseline_requirements,
|
||||
system_overlay_requirements,
|
||||
enterprise_managed_requirements,
|
||||
} = bundle_layers;
|
||||
|
||||
compose_requirements(enterprise_managed_requirements).map_err(|err| {
|
||||
CloudConfigBundleLoadError::new(
|
||||
CloudConfigBundleLoadErrorCode::InvalidBundle,
|
||||
/*status_code*/ None,
|
||||
format!("invalid cloud config bundle: {err}"),
|
||||
)
|
||||
})?;
|
||||
for requirements_layers in [
|
||||
baseline_requirements.unwrap_or_default(),
|
||||
system_overlay_requirements.unwrap_or_default(),
|
||||
enterprise_managed_requirements,
|
||||
] {
|
||||
compose_requirements(requirements_layers).map_err(|err| {
|
||||
CloudConfigBundleLoadError::new(
|
||||
CloudConfigBundleLoadErrorCode::InvalidBundle,
|
||||
/*status_code*/ None,
|
||||
format!("invalid cloud config bundle: {err}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -21,12 +21,20 @@ pub struct DeliveredConfigToml {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub enterprise_managed: Option<Option<Vec<models::DeliveredTomlFragment>>>,
|
||||
#[serde(
|
||||
rename = "managed_layers",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub managed_layers: Option<Option<Box<models::DeliveredManagedLayers>>>,
|
||||
}
|
||||
|
||||
impl DeliveredConfigToml {
|
||||
pub fn new() -> DeliveredConfigToml {
|
||||
DeliveredConfigToml {
|
||||
enterprise_managed: None,
|
||||
managed_layers: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* codex-backend
|
||||
*
|
||||
* codex-backend
|
||||
*
|
||||
* The version of the OpenAPI document: 0.0.1
|
||||
*
|
||||
* Generated by: https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
use crate::models;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DeliveredManagedLayers {
|
||||
#[serde(
|
||||
rename = "baseline",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub baseline: Option<Option<Vec<models::DeliveredTomlFragment>>>,
|
||||
#[serde(
|
||||
rename = "system_overlay",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub system_overlay: Option<Option<Vec<models::DeliveredTomlFragment>>>,
|
||||
}
|
||||
|
||||
impl DeliveredManagedLayers {
|
||||
pub fn new() -> DeliveredManagedLayers {
|
||||
DeliveredManagedLayers {
|
||||
baseline: None,
|
||||
system_overlay: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,20 @@ pub struct DeliveredRequirementsToml {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub enterprise_managed: Option<Option<Vec<models::DeliveredTomlFragment>>>,
|
||||
#[serde(
|
||||
rename = "managed_layers",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub managed_layers: Option<Option<Box<models::DeliveredManagedLayers>>>,
|
||||
}
|
||||
|
||||
impl DeliveredRequirementsToml {
|
||||
pub fn new() -> DeliveredRequirementsToml {
|
||||
DeliveredRequirementsToml {
|
||||
enterprise_managed: None,
|
||||
managed_layers: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ pub use self::config_file_response::ConfigFileResponse;
|
||||
pub(crate) mod delivered_config_toml;
|
||||
pub use self::delivered_config_toml::DeliveredConfigToml;
|
||||
|
||||
pub(crate) mod delivered_managed_layers;
|
||||
pub use self::delivered_managed_layers::DeliveredManagedLayers;
|
||||
|
||||
pub(crate) mod delivered_requirements_toml;
|
||||
pub use self::delivered_requirements_toml::DeliveredRequirementsToml;
|
||||
|
||||
|
||||
@@ -35,23 +35,75 @@ impl CloudConfigBundle {
|
||||
} = self;
|
||||
let CloudConfigTomlBundle {
|
||||
enterprise_managed: config_enterprise_managed,
|
||||
managed_layers: config_managed_layers,
|
||||
} = config_toml;
|
||||
let CloudRequirementsTomlBundle {
|
||||
enterprise_managed: requirements_enterprise_managed,
|
||||
managed_layers: requirements_managed_layers,
|
||||
} = requirements_toml;
|
||||
|
||||
config_enterprise_managed.is_empty() && requirements_enterprise_managed.is_empty()
|
||||
config_enterprise_managed.is_empty()
|
||||
&& config_managed_layers.is_absent()
|
||||
&& requirements_enterprise_managed.is_empty()
|
||||
&& requirements_managed_layers.is_absent()
|
||||
}
|
||||
|
||||
/// Returns true when any managed bucket was present in the backend payload.
|
||||
///
|
||||
/// An explicitly empty bucket still counts as present because it can suppress
|
||||
/// compatibility fallback behavior when the bundle is materialized.
|
||||
pub fn has_managed_layer_buckets(&self) -> bool {
|
||||
!self.config_toml.managed_layers.is_absent()
|
||||
|| !self.requirements_toml.managed_layers.is_absent()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct CloudConfigTomlBundle {
|
||||
pub enterprise_managed: Vec<CloudConfigFragment>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "CloudConfigTomlManagedLayers::is_absent"
|
||||
)]
|
||||
pub managed_layers: CloudConfigTomlManagedLayers,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct CloudConfigTomlManagedLayers {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub baseline: Option<Vec<CloudConfigFragment>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub system_overlay: Option<Vec<CloudConfigFragment>>,
|
||||
}
|
||||
|
||||
impl CloudConfigTomlManagedLayers {
|
||||
fn is_absent(&self) -> bool {
|
||||
self.baseline.is_none() && self.system_overlay.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct CloudRequirementsTomlBundle {
|
||||
pub enterprise_managed: Vec<CloudRequirementsFragment>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "CloudRequirementsTomlManagedLayers::is_absent"
|
||||
)]
|
||||
pub managed_layers: CloudRequirementsTomlManagedLayers,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct CloudRequirementsTomlManagedLayers {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub baseline: Option<Vec<CloudRequirementsFragment>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub system_overlay: Option<Vec<CloudRequirementsFragment>>,
|
||||
}
|
||||
|
||||
impl CloudRequirementsTomlManagedLayers {
|
||||
fn is_absent(&self) -> bool {
|
||||
self.baseline.is_none() && self.system_overlay.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
@@ -67,8 +119,16 @@ pub struct CloudRequirementsFragment {
|
||||
/// inserted relative to local/system/user layers.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CloudConfigBundleLayers {
|
||||
/// Baseline config layers in `ConfigLayerStack` order, when delivered.
|
||||
pub baseline_config: Option<Vec<ConfigLayerEntry>>,
|
||||
/// System-overlay config layers in `ConfigLayerStack` order, when delivered.
|
||||
pub system_overlay_config: Option<Vec<ConfigLayerEntry>>,
|
||||
/// Enterprise-managed config layers in `ConfigLayerStack` order.
|
||||
pub enterprise_managed_config: Vec<ConfigLayerEntry>,
|
||||
/// Baseline requirements layers in requirements merge order, when delivered.
|
||||
pub baseline_requirements: Option<Vec<RequirementsLayerEntry>>,
|
||||
/// System-overlay requirements layers in requirements merge order, when delivered.
|
||||
pub system_overlay_requirements: Option<Vec<RequirementsLayerEntry>>,
|
||||
/// Enterprise-managed requirements layers in requirements layer merge order.
|
||||
pub enterprise_managed_requirements: Vec<RequirementsLayerEntry>,
|
||||
}
|
||||
@@ -99,43 +159,77 @@ impl CloudConfigBundleLayers {
|
||||
config_toml:
|
||||
CloudConfigTomlBundle {
|
||||
enterprise_managed: config_enterprise_managed,
|
||||
managed_layers:
|
||||
CloudConfigTomlManagedLayers {
|
||||
baseline: config_baseline,
|
||||
system_overlay: config_system_overlay,
|
||||
},
|
||||
},
|
||||
requirements_toml:
|
||||
CloudRequirementsTomlBundle {
|
||||
enterprise_managed: requirements_enterprise_managed,
|
||||
managed_layers:
|
||||
CloudRequirementsTomlManagedLayers {
|
||||
baseline: requirements_baseline,
|
||||
system_overlay: requirements_system_overlay,
|
||||
},
|
||||
},
|
||||
} = 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 parse_config_fragments = |fragments| {
|
||||
if strict_config {
|
||||
cloud_config_layers_from_fragments_strict(fragments, base_dir)
|
||||
} else {
|
||||
cloud_config_layers_from_fragments(fragments, base_dir)
|
||||
}
|
||||
};
|
||||
let baseline_config = config_baseline.map(parse_config_fragments).transpose()?;
|
||||
let system_overlay_config = config_system_overlay
|
||||
.map(parse_config_fragments)
|
||||
.transpose()?;
|
||||
let enterprise_managed_config = parse_config_fragments(config_enterprise_managed)?;
|
||||
|
||||
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::<Vec<_>>();
|
||||
// Bundle fragments arrive highest-priority first, while requirements
|
||||
// layers are merged lowest-priority to highest-priority.
|
||||
enterprise_managed_requirements.reverse();
|
||||
let baseline_requirements = requirements_baseline
|
||||
.map(|fragments| requirements_layers_from_fragments(fragments, base_dir));
|
||||
let system_overlay_requirements = requirements_system_overlay
|
||||
.map(|fragments| requirements_layers_from_fragments(fragments, base_dir));
|
||||
let enterprise_managed_requirements =
|
||||
requirements_layers_from_fragments(requirements_enterprise_managed, base_dir);
|
||||
|
||||
Ok(Self {
|
||||
baseline_config,
|
||||
system_overlay_config,
|
||||
enterprise_managed_config,
|
||||
baseline_requirements,
|
||||
system_overlay_requirements,
|
||||
enterprise_managed_requirements,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn requirements_layers_from_fragments(
|
||||
fragments: Vec<CloudRequirementsFragment>,
|
||||
base_dir: &AbsolutePathBuf,
|
||||
) -> Vec<RequirementsLayerEntry> {
|
||||
let mut layers = fragments
|
||||
.into_iter()
|
||||
.map(|fragment| {
|
||||
RequirementsLayerEntry::from_toml(
|
||||
RequirementSource::EnterpriseManaged {
|
||||
id: fragment.id,
|
||||
name: fragment.name,
|
||||
},
|
||||
fragment.contents,
|
||||
)
|
||||
.with_base_dir(base_dir.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Bundle fragments arrive highest-priority first, while requirements
|
||||
// layers are merged lowest-priority to highest-priority.
|
||||
layers.reverse();
|
||||
layers
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CloudConfigBundleLoadErrorCode {
|
||||
Auth,
|
||||
|
||||
@@ -23,6 +23,23 @@ async fn shared_future_runs_once() {
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_with_explicitly_empty_overlay_is_not_empty() {
|
||||
let bundle = CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle {
|
||||
enterprise_managed: Vec::new(),
|
||||
managed_layers: CloudConfigTomlManagedLayers {
|
||||
baseline: None,
|
||||
system_overlay: Some(Vec::new()),
|
||||
},
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle::default(),
|
||||
};
|
||||
|
||||
assert!(!bundle.is_empty());
|
||||
assert!(bundle.has_managed_layer_buckets());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_layers_preserve_enterprise_managed_bucket_order() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
@@ -42,6 +59,7 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() {
|
||||
contents: "model = \"low\"".to_string(),
|
||||
},
|
||||
],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: vec![
|
||||
@@ -56,6 +74,7 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() {
|
||||
contents: "allowed_approval_policies = [\"never\"]".to_string(),
|
||||
},
|
||||
],
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
},
|
||||
&base_dir,
|
||||
@@ -92,20 +111,101 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_layers_can_strict_validate_enterprise_managed_config() {
|
||||
fn bundle_layers_preserve_managed_bucket_presence_and_order() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let base_dir = AbsolutePathBuf::from_absolute_path(tempdir.path()).expect("absolute path");
|
||||
let layers = CloudConfigBundleLayers::from_bundle(
|
||||
CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle {
|
||||
enterprise_managed: Vec::new(),
|
||||
managed_layers: CloudConfigTomlManagedLayers {
|
||||
baseline: Some(vec![
|
||||
CloudConfigFragment {
|
||||
id: "baseline_high".to_string(),
|
||||
name: "Baseline high".to_string(),
|
||||
contents: "model = \"high\"".to_string(),
|
||||
},
|
||||
CloudConfigFragment {
|
||||
id: "baseline_low".to_string(),
|
||||
name: "Baseline low".to_string(),
|
||||
contents: "model = \"low\"".to_string(),
|
||||
},
|
||||
]),
|
||||
system_overlay: Some(Vec::new()),
|
||||
},
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: Vec::new(),
|
||||
managed_layers: CloudRequirementsTomlManagedLayers {
|
||||
baseline: None,
|
||||
system_overlay: Some(vec![CloudRequirementsFragment {
|
||||
id: "overlay".to_string(),
|
||||
name: "Overlay".to_string(),
|
||||
contents: "allowed_approval_policies = [\"never\"]".to_string(),
|
||||
}]),
|
||||
},
|
||||
},
|
||||
},
|
||||
&base_dir,
|
||||
)
|
||||
.expect("bundle should be converted into layers");
|
||||
|
||||
assert_eq!(
|
||||
layers
|
||||
.baseline_config
|
||||
.expect("baseline should be present")
|
||||
.into_iter()
|
||||
.map(|layer| layer.name)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
ConfigLayerSource::EnterpriseManaged {
|
||||
id: "baseline_low".to_string(),
|
||||
name: "Baseline low".to_string(),
|
||||
},
|
||||
ConfigLayerSource::EnterpriseManaged {
|
||||
id: "baseline_high".to_string(),
|
||||
name: "Baseline high".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(layers.system_overlay_config, Some(Vec::new()));
|
||||
assert!(layers.baseline_requirements.is_none());
|
||||
assert_eq!(
|
||||
compose_requirements(
|
||||
layers
|
||||
.system_overlay_requirements
|
||||
.expect("system overlay should be present")
|
||||
)
|
||||
.expect("requirements should compose")
|
||||
.expect("requirements should be present")
|
||||
.into_toml(),
|
||||
ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_layers_can_strict_validate_managed_config() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let base_dir = AbsolutePathBuf::from_absolute_path(tempdir.path()).expect("absolute path");
|
||||
let err = CloudConfigBundleLayers::from_bundle_strict_config(
|
||||
CloudConfigBundle {
|
||||
config_toml: CloudConfigTomlBundle {
|
||||
enterprise_managed: vec![CloudConfigFragment {
|
||||
id: "cfg".to_string(),
|
||||
name: "Cloud config".to_string(),
|
||||
contents: "unknown_key = true".to_string(),
|
||||
}],
|
||||
enterprise_managed: Vec::new(),
|
||||
managed_layers: CloudConfigTomlManagedLayers {
|
||||
baseline: Some(vec![CloudConfigFragment {
|
||||
id: "cfg".to_string(),
|
||||
name: "Cloud config".to_string(),
|
||||
contents: "unknown_key = true".to_string(),
|
||||
}]),
|
||||
system_overlay: None,
|
||||
},
|
||||
},
|
||||
requirements_toml: CloudRequirementsTomlBundle {
|
||||
enterprise_managed: Vec::new(),
|
||||
managed_layers: Default::default(),
|
||||
},
|
||||
},
|
||||
&base_dir,
|
||||
|
||||
@@ -38,8 +38,10 @@ pub use cloud_config_bundle::CloudConfigBundleLoadError;
|
||||
pub use cloud_config_bundle::CloudConfigBundleLoadErrorCode;
|
||||
pub use cloud_config_bundle::CloudConfigBundleLoader;
|
||||
pub use cloud_config_bundle::CloudConfigTomlBundle;
|
||||
pub use cloud_config_bundle::CloudConfigTomlManagedLayers;
|
||||
pub use cloud_config_bundle::CloudRequirementsFragment;
|
||||
pub use cloud_config_bundle::CloudRequirementsTomlBundle;
|
||||
pub use cloud_config_bundle::CloudRequirementsTomlManagedLayers;
|
||||
pub use cloud_config_layers::CloudConfigFragment;
|
||||
pub use cloud_config_layers::CloudConfigFragmentSource;
|
||||
pub use cloud_config_layers::CloudConfigLayerError;
|
||||
|
||||
@@ -146,7 +146,11 @@ pub async fn load_config_layers_state(
|
||||
CloudConfigBundleLayers::from_bundle(bundle, &cloud_config_base_dir)?
|
||||
};
|
||||
let CloudConfigBundleLayers {
|
||||
baseline_config: _,
|
||||
system_overlay_config: _,
|
||||
enterprise_managed_config,
|
||||
baseline_requirements: _,
|
||||
system_overlay_requirements: _,
|
||||
enterprise_managed_requirements,
|
||||
} = bundle_layers;
|
||||
bundle_requirements_layers = enterprise_managed_requirements;
|
||||
|
||||
Reference in New Issue
Block a user