Skip to content

Commit 6358d0a

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 4a9076f commit 6358d0a

6 files changed

Lines changed: 124 additions & 6 deletions

File tree

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_CHAIN_PARAMS_PATH: &str = "testing/chain_params.libsonnet";
@@ -108,6 +114,96 @@ where
108114
}
109115
}
110116

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