Skip to content

Commit df3b806

Browse files
apollo_deployment: check applicative config matches deployed app_configs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f7c1bae commit df3b806

7 files changed

Lines changed: 125 additions & 7 deletions

File tree

crates/apollo_deployments/jsonnet/lib/applicative_config.libsonnet

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ function(overrides)
488488
l1_events_scraper_config: {
489489
chain_id: chainId,
490490
finality: 10,
491-
l1_block_time_seconds: 12.0,
491+
l1_block_time_seconds: 12,
492492
polling_interval_seconds: 30.0,
493493
set_provider_historic_height_to_l2_genesis: false,
494494
startup_rewind_time_seconds: 21600.0,

crates/apollo_deployments/resources/app_configs/l1_events_scraper_config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"l1_events_scraper_config.finality": 10,
3-
"l1_events_scraper_config.l1_block_time_seconds": 12.0,
3+
"l1_events_scraper_config.l1_block_time_seconds": 12,
44
"l1_events_scraper_config.polling_interval_seconds": 30,
55
"l1_events_scraper_config.set_provider_historic_height_to_l2_genesis": false,
66
"l1_events_scraper_config.startup_rewind_time_seconds": 21600

crates/apollo_deployments/resources/app_configs/replacer_l1_events_scraper_config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"l1_events_scraper_config.finality": 10,
3-
"l1_events_scraper_config.l1_block_time_seconds": 12.0,
3+
"l1_events_scraper_config.l1_block_time_seconds": 12,
44
"l1_events_scraper_config.polling_interval_seconds": 30,
55
"l1_events_scraper_config.set_provider_historic_height_to_l2_genesis": false,
66
"l1_events_scraper_config.startup_rewind_time_seconds": 21600

crates/apollo_deployments/src/deployment_definitions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ mod deployment_definitions_test;
88
pub(crate) const CONFIG_BASE_DIR: &str = "crates/apollo_deployments/resources/";
99
pub(crate) const RETRIES_FOR_L1_SERVICES: usize = 0;
1010

11-
const BASE_APP_CONFIGS_DIR_PATH: &str = "crates/apollo_deployments/resources/app_configs";
11+
pub(crate) const BASE_APP_CONFIGS_DIR_PATH: &str =
12+
"crates/apollo_deployments/resources/app_configs";
1213

1314
#[derive(
1415
Hash, Clone, Debug, Display, Serialize, PartialEq, Eq, PartialOrd, Ord, EnumIter, AsRefStr,

crates/apollo_deployments/src/deployment_definitions_test.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,28 @@ use crate::deployment_definitions::ComponentConfigInService;
1313
use crate::deployments::consolidated::ConsolidatedNodeServiceName;
1414
use crate::deployments::distributed::DistributedNodeServiceName;
1515
use crate::deployments::hybrid::HybridNodeServiceName;
16-
use crate::jsonnet::{assert_build_deserializes, assert_infra_matches_rust};
16+
use crate::jsonnet::{
17+
assert_build_deserializes,
18+
assert_infra_matches_rust,
19+
test_applicative_matches_app_configs,
20+
};
1721
use crate::service::NodeType;
1822
use crate::test_utils::SecretsConfigOverride;
1923

2024
const SECRETS_FOR_TESTING_ENV_PATH: &str =
2125
"crates/apollo_deployments/resources/testing_secrets.json";
2226

27+
/// Verifies the applicative config emitted by jsonnet matches the committed `app_configs/*.json`
28+
/// (the deployment's non-overridable value layer), up to overridable keys, secrets, and integers
29+
/// jsonnet can't represent.
30+
#[test]
31+
fn applicative_matches_app_configs() {
32+
env::set_current_dir(resolve_project_relative_path("").unwrap())
33+
.expect("Couldn't set working dir.");
34+
35+
test_applicative_matches_app_configs();
36+
}
37+
2338
/// Verifies the jsonnet hybrid infra config matches the Rust deployment definitions (hybrid.rs).
2439
#[test]
2540
fn hybrid_infra_matches_rust() {

crates/apollo_deployments/src/jsonnet.rs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
use std::collections::{BTreeMap, BTreeSet};
12
use std::path::PathBuf;
23

3-
use apollo_node_config::node_config::SequencerNodeConfig;
4+
use apollo_config::dumping::SerializeConfig;
5+
use apollo_config::{FIELD_SEPARATOR, IS_NONE_MARK};
6+
use apollo_node_config::config_utils::{config_to_preset, private_parameters};
7+
use apollo_node_config::node_config::{SequencerNodeConfig, CONFIG_POINTERS};
48
use jrsonnet_evaluator::trace::PathResolver;
59
use jrsonnet_evaluator::{FileImportResolver, State};
610
use serde_json::Value;
711
use strum::IntoEnumIterator;
812

9-
use crate::service::{GetComponentConfigs, NodeService, NodeType};
13+
use crate::deployment_definitions::BASE_APP_CONFIGS_DIR_PATH;
14+
use crate::service::{GetComponentConfigs, NodeService, NodeType, KEYS_TO_BE_REPLACED};
15+
use crate::test_utils::is_path_prefix;
1016

1117
const JSONNET_DIR: &str = "crates/apollo_deployments/jsonnet";
1218
const TESTING_OVERRIDES_PATH: &str = "testing/overrides.libsonnet";
@@ -102,6 +108,96 @@ where
102108
}
103109
}
104110

111+
/// Asserts the applicative config emitted by jsonnet reproduces the committed `app_configs/*.json`
112+
/// for every keys, except keys that are overridable, secret, or under `components.*`.
113+
pub fn test_applicative_matches_app_configs() {
114+
// Applicative side: the single consolidated `node` service carries every component's business
115+
// config; round-trip through the config struct and render it in the app_configs preset format.
116+
let built = eval_build("consolidated", TESTING_OVERRIDES_PATH);
117+
let node = built.get("node").expect("consolidated has a `node` service").clone();
118+
let parsed: SequencerNodeConfig = serde_json::from_value(node).unwrap();
119+
let build_preset = config_to_preset(&serde_json::json!(parsed.dump()));
120+
let build_map = build_preset.as_object().unwrap();
121+
122+
let excluded = non_default_paths();
123+
let is_excluded = |path: &str| {
124+
is_path_prefix("components", path) || excluded.iter().any(|key| is_path_prefix(key, path))
125+
};
126+
127+
let app_config_map = merged_app_configs();
128+
129+
let mut mismatches = Vec::new();
130+
for (key, app_config_value) in &app_config_map {
131+
if is_excluded(key) {
132+
continue;
133+
}
134+
match build_map.get(key) {
135+
Some(build_value) => {
136+
if build_value != app_config_value {
137+
mismatches.push(format!(
138+
"{key}: applicative={build_value} app_config={app_config_value}"
139+
));
140+
}
141+
}
142+
None => mismatches
143+
.push(format!("{key}: missing in applicative (app_config={app_config_value})")),
144+
}
145+
}
146+
147+
assert!(
148+
mismatches.is_empty(),
149+
"applicative config diverges from app_configs/*.json at {} non-overridable, non-secret \
150+
keys:\n {}",
151+
mismatches.len(),
152+
mismatches.join("\n ")
153+
);
154+
}
155+
156+
/// Merges every base `app_configs/<component>.json` (skipping the derived `replacer_*` files) into
157+
/// a single flat dotted-key map.
158+
fn merged_app_configs() -> BTreeMap<String, Value> {
159+
let mut app_config_map: BTreeMap<String, Value> = BTreeMap::new();
160+
for entry in std::fs::read_dir(BASE_APP_CONFIGS_DIR_PATH).expect("app_configs dir exists") {
161+
let path = entry.expect("readable dir entry").path();
162+
let is_json = path.extension().is_some_and(|extension| extension == "json");
163+
let is_replacer = path.file_name().unwrap().to_string_lossy().starts_with("replacer_");
164+
if !is_json || is_replacer {
165+
continue;
166+
}
167+
let contents = std::fs::read_to_string(&path).expect("app_config file is readable");
168+
let object: serde_json::Map<String, Value> =
169+
serde_json::from_str(&contents).expect("app_config is a JSON object");
170+
app_config_map.extend(object);
171+
}
172+
app_config_map
173+
}
174+
175+
/// The config paths that are overridable or secrets or passed as pointers.
176+
fn non_default_paths() -> BTreeSet<String> {
177+
// An optional config is marked overridable/secret as `<path>.#is_none`; the override replaces
178+
// the whole option, so exclude the `<path>` subtree (not just the marker).
179+
let is_none_suffix = format!("{FIELD_SEPARATOR}{IS_NONE_MARK}");
180+
let insert_with_option_root = |paths: &mut BTreeSet<String>, key: &str| {
181+
paths.insert(key.to_string());
182+
if let Some(option_root) = key.strip_suffix(&is_none_suffix) {
183+
paths.insert(option_root.to_string());
184+
}
185+
};
186+
187+
let mut paths = BTreeSet::new();
188+
for key in KEYS_TO_BE_REPLACED.iter() {
189+
insert_with_option_root(&mut paths, key);
190+
}
191+
for ((target_path, _param), pointing_paths) in CONFIG_POINTERS.iter() {
192+
paths.insert(target_path.clone());
193+
paths.extend(pointing_paths.iter().cloned());
194+
}
195+
for key in private_parameters() {
196+
insert_with_option_root(&mut paths, &key);
197+
}
198+
paths
199+
}
200+
105201
/// Clones a `components` map with `url` and `port` removed from each component object — the two
106202
/// fields the Rust config leaves as deploy-time placeholders, so they can't be compared against the
107203
/// jsonnet's baked-in real values.

crates/apollo_deployments/src/test_utils.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ use url::Url;
99

1010
pub(crate) const FIX_BINARY_NAME: &str = "deployment_generator";
1111

12+
/// Returns `true` if `prefix` is a path-prefix of the dotted config key `path`: either the same key
13+
/// or a dot-bounded ancestor of it (so `range_check` does not match the sibling `range_check96`).
14+
pub(crate) fn is_path_prefix(prefix: &str, path: &str) -> bool {
15+
path.strip_prefix(prefix).is_some_and(|rest| rest.is_empty() || rest.starts_with('.'))
16+
}
17+
1218
#[derive(Serialize)]
1319
pub struct SecretsConfigOverride {
1420
#[serde(

0 commit comments

Comments
 (0)