Skip to content

Commit 8e48f00

Browse files
committed
use merge instead of extend with KafkaBrokerConfigOverrides
1 parent b6a01a0 commit 8e48f00

3 files changed

Lines changed: 144 additions & 131 deletions

File tree

extra/crds.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -543,23 +543,23 @@ spec:
543543
additionalProperties:
544544
nullable: true
545545
type: string
546+
default: {}
546547
description: |-
547548
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
548549
549550
This is backwards-compatible with the existing flat key-value YAML format
550551
used by `HashMap<String, String>`.
551-
nullable: true
552552
type: object
553553
security.properties:
554554
additionalProperties:
555555
nullable: true
556556
type: string
557+
default: {}
557558
description: |-
558559
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
559560
560561
This is backwards-compatible with the existing flat key-value YAML format
561562
used by `HashMap<String, String>`.
562-
nullable: true
563563
type: object
564564
type: object
565565
envOverrides:
@@ -1162,23 +1162,23 @@ spec:
11621162
additionalProperties:
11631163
nullable: true
11641164
type: string
1165+
default: {}
11651166
description: |-
11661167
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
11671168
11681169
This is backwards-compatible with the existing flat key-value YAML format
11691170
used by `HashMap<String, String>`.
1170-
nullable: true
11711171
type: object
11721172
security.properties:
11731173
additionalProperties:
11741174
nullable: true
11751175
type: string
1176+
default: {}
11761177
description: |-
11771178
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
11781179
11791180
This is backwards-compatible with the existing flat key-value YAML format
11801181
used by `HashMap<String, String>`.
1181-
nullable: true
11821182
type: object
11831183
type: object
11841184
envOverrides:
@@ -1792,23 +1792,23 @@ spec:
17921792
additionalProperties:
17931793
nullable: true
17941794
type: string
1795+
default: {}
17951796
description: |-
17961797
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
17971798
17981799
This is backwards-compatible with the existing flat key-value YAML format
17991800
used by `HashMap<String, String>`.
1800-
nullable: true
18011801
type: object
18021802
security.properties:
18031803
additionalProperties:
18041804
nullable: true
18051805
type: string
1806+
default: {}
18061807
description: |-
18071808
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
18081809
18091810
This is backwards-compatible with the existing flat key-value YAML format
18101811
used by `HashMap<String, String>`.
1811-
nullable: true
18121812
type: object
18131813
type: object
18141814
envOverrides:
@@ -2243,23 +2243,23 @@ spec:
22432243
additionalProperties:
22442244
nullable: true
22452245
type: string
2246+
default: {}
22462247
description: |-
22472248
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
22482249
22492250
This is backwards-compatible with the existing flat key-value YAML format
22502251
used by `HashMap<String, String>`.
2251-
nullable: true
22522252
type: object
22532253
security.properties:
22542254
additionalProperties:
22552255
nullable: true
22562256
type: string
2257+
default: {}
22572258
description: |-
22582259
Flat key-value overrides for `*.properties`, Hadoop XML, etc.
22592260
22602261
This is backwards-compatible with the existing flat key-value YAML format
22612262
used by `HashMap<String, String>`.
2262-
nullable: true
22632263
type: object
22642264
type: object
22652265
envOverrides:

rust/operator-binary/src/controller/validate.rs

Lines changed: 122 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
use std::collections::BTreeMap;
77

88
use snafu::{ResultExt, Snafu};
9-
use stackable_operator::{cli::OperatorEnvironmentOptions, commons::product_image_selection};
9+
use stackable_operator::{
10+
cli::OperatorEnvironmentOptions,
11+
commons::product_image_selection,
12+
config::merge::{Merge, merge},
13+
v2::config_overrides::KeyValueConfigOverrides,
14+
};
1015

1116
use crate::{
1217
controller::{
@@ -169,12 +174,30 @@ pub fn validate(
169174
})
170175
}
171176

172-
// DESIGN DECISION: role-group overrides are merged role-level first, then role-group
173-
// extended on top so role-group wins — identical to the precedent product-config used.
174-
// We read the v2 KeyValueConfigOverrides `.overrides` map and BTreeMap::extend rather
175-
// than using its `Merge` impl, because plain extend reproduces the old behaviour exactly
176-
// (last-writer-wins per key) and avoids depending on Merge semantics. Alternative:
177-
// KeyValueConfigOverrides::merge — equivalent here but an unnecessary semantic dependency.
177+
/// Merge role-group overrides over the role-level overrides (role-group wins per key) via the
178+
/// `Merge` impl derived on the override structs.
179+
///
180+
/// NOTE on semantics: `Merge` treats a role-group `null` value as "inherit the role-level value",
181+
/// *not* "unset it". This differs from `main`'s product-config layering, which `.extend()`ed the
182+
/// maps so a role-group `null` *removed* a role-level key. The `tests` module has a worked
183+
/// example of the difference.
184+
fn merge_role_group_overrides<O: Merge + Clone>(role: &O, role_group: Option<&O>) -> O {
185+
match role_group {
186+
Some(role_group) => merge(role_group.clone(), role),
187+
None => role.clone(),
188+
}
189+
}
190+
191+
/// Flatten resolved key/value overrides into a plain map, dropping entries whose value is
192+
/// unset (`null`).
193+
fn flatten_overrides(overrides: KeyValueConfigOverrides) -> BTreeMap<String, String> {
194+
overrides
195+
.overrides
196+
.into_iter()
197+
.filter_map(|(key, value)| value.map(|value| (key, value)))
198+
.collect()
199+
}
200+
178201
fn collect_broker_role_group_overrides(
179202
kafka: &v1alpha1::KafkaCluster,
180203
broker_role: &crate::crd::BrokerRole,
@@ -184,47 +207,15 @@ fn collect_broker_role_group_overrides(
184207
BTreeMap<String, String>,
185208
BTreeMap<String, String>,
186209
) {
187-
// --- broker.properties overrides ---
188-
let role_broker_overrides: BTreeMap<String, Option<String>> = broker_role
189-
.config
190-
.config_overrides
191-
.broker_properties
192-
.as_ref()
193-
.map(|o| o.overrides.clone())
194-
.unwrap_or_default();
195-
let rg_broker_overrides: BTreeMap<String, Option<String>> = broker_role
196-
.role_groups
197-
.get(rolegroup_name)
198-
.and_then(|rg| rg.config.config_overrides.broker_properties.as_ref())
199-
.map(|o| o.overrides.clone())
200-
.unwrap_or_default();
201-
let mut merged_broker = role_broker_overrides;
202-
merged_broker.extend(rg_broker_overrides);
203-
let config_file_overrides: BTreeMap<String, String> = merged_broker
204-
.into_iter()
205-
.filter_map(|(k, v)| v.map(|v| (k, v)))
206-
.collect();
207-
208-
// --- security.properties overrides ---
209-
let role_security_overrides: BTreeMap<String, Option<String>> = broker_role
210-
.config
211-
.config_overrides
212-
.security_properties
213-
.as_ref()
214-
.map(|o| o.overrides.clone())
215-
.unwrap_or_default();
216-
let rg_security_overrides: BTreeMap<String, Option<String>> = broker_role
217-
.role_groups
218-
.get(rolegroup_name)
219-
.and_then(|rg| rg.config.config_overrides.security_properties.as_ref())
220-
.map(|o| o.overrides.clone())
221-
.unwrap_or_default();
222-
let mut merged_security = role_security_overrides;
223-
merged_security.extend(rg_security_overrides);
224-
let jvm_security_overrides: BTreeMap<String, String> = merged_security
225-
.into_iter()
226-
.filter_map(|(k, v)| v.map(|v| (k, v)))
227-
.collect();
210+
let merged_overrides = merge_role_group_overrides(
211+
&broker_role.config.config_overrides,
212+
broker_role
213+
.role_groups
214+
.get(rolegroup_name)
215+
.map(|rg| &rg.config.config_overrides),
216+
);
217+
let config_file_overrides = flatten_overrides(merged_overrides.broker_properties);
218+
let jvm_security_overrides = flatten_overrides(merged_overrides.security_properties);
228219

229220
// --- env overrides ---
230221
// DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides
@@ -252,12 +243,6 @@ fn collect_broker_role_group_overrides(
252243
(config_file_overrides, jvm_security_overrides, env_overrides)
253244
}
254245

255-
// DESIGN DECISION: role-group overrides are merged role-level first, then role-group
256-
// extended on top so role-group wins — identical to the precedent product-config used.
257-
// We read the v2 KeyValueConfigOverrides `.overrides` map and BTreeMap::extend rather
258-
// than using its `Merge` impl, because plain extend reproduces the old behaviour exactly
259-
// (last-writer-wins per key) and avoids depending on Merge semantics. Alternative:
260-
// KeyValueConfigOverrides::merge — equivalent here but an unnecessary semantic dependency.
261246
fn collect_controller_role_group_overrides(
262247
kafka: &v1alpha1::KafkaCluster,
263248
controller_role: &crate::crd::ControllerRole,
@@ -267,47 +252,15 @@ fn collect_controller_role_group_overrides(
267252
BTreeMap<String, String>,
268253
BTreeMap<String, String>,
269254
) {
270-
// --- controller.properties overrides ---
271-
let role_controller_overrides: BTreeMap<String, Option<String>> = controller_role
272-
.config
273-
.config_overrides
274-
.controller_properties
275-
.as_ref()
276-
.map(|o| o.overrides.clone())
277-
.unwrap_or_default();
278-
let rg_controller_overrides: BTreeMap<String, Option<String>> = controller_role
279-
.role_groups
280-
.get(rolegroup_name)
281-
.and_then(|rg| rg.config.config_overrides.controller_properties.as_ref())
282-
.map(|o| o.overrides.clone())
283-
.unwrap_or_default();
284-
let mut merged_controller = role_controller_overrides;
285-
merged_controller.extend(rg_controller_overrides);
286-
let config_file_overrides: BTreeMap<String, String> = merged_controller
287-
.into_iter()
288-
.filter_map(|(k, v)| v.map(|v| (k, v)))
289-
.collect();
290-
291-
// --- security.properties overrides ---
292-
let role_security_overrides: BTreeMap<String, Option<String>> = controller_role
293-
.config
294-
.config_overrides
295-
.security_properties
296-
.as_ref()
297-
.map(|o| o.overrides.clone())
298-
.unwrap_or_default();
299-
let rg_security_overrides: BTreeMap<String, Option<String>> = controller_role
300-
.role_groups
301-
.get(rolegroup_name)
302-
.and_then(|rg| rg.config.config_overrides.security_properties.as_ref())
303-
.map(|o| o.overrides.clone())
304-
.unwrap_or_default();
305-
let mut merged_security = role_security_overrides;
306-
merged_security.extend(rg_security_overrides);
307-
let jvm_security_overrides: BTreeMap<String, String> = merged_security
308-
.into_iter()
309-
.filter_map(|(k, v)| v.map(|v| (k, v)))
310-
.collect();
255+
let merged_overrides = merge_role_group_overrides(
256+
&controller_role.config.config_overrides,
257+
controller_role
258+
.role_groups
259+
.get(rolegroup_name)
260+
.map(|rg| &rg.config.config_overrides),
261+
);
262+
let config_file_overrides = flatten_overrides(merged_overrides.controller_properties);
263+
let jvm_security_overrides = flatten_overrides(merged_overrides.security_properties);
311264

312265
// --- env overrides ---
313266
// DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides
@@ -335,3 +288,77 @@ fn collect_controller_role_group_overrides(
335288

336289
(config_file_overrides, jvm_security_overrides, env_overrides)
337290
}
291+
292+
#[cfg(test)]
293+
mod tests {
294+
use std::collections::BTreeMap;
295+
296+
use stackable_operator::v2::config_overrides::KeyValueConfigOverrides;
297+
298+
use super::{flatten_overrides, merge_role_group_overrides};
299+
300+
/// Build a `KeyValueConfigOverrides` from `(key, value)` pairs, where a `None` value
301+
/// represents an explicit `null` (unset) in the CRD.
302+
fn overrides(pairs: &[(&str, Option<&str>)]) -> KeyValueConfigOverrides {
303+
KeyValueConfigOverrides {
304+
overrides: pairs
305+
.iter()
306+
.map(|(key, value)| (key.to_string(), value.map(str::to_string)))
307+
.collect(),
308+
}
309+
}
310+
311+
/// Run the full role/role-group resolution (merge then flatten) for a single config file.
312+
fn resolve(
313+
role: KeyValueConfigOverrides,
314+
role_group: Option<KeyValueConfigOverrides>,
315+
) -> BTreeMap<String, String> {
316+
flatten_overrides(merge_role_group_overrides(&role, role_group.as_ref()))
317+
}
318+
319+
#[test]
320+
fn role_group_value_wins_over_role() {
321+
let role = overrides(&[("a", Some("role")), ("b", Some("role-only"))]);
322+
let role_group = overrides(&[("a", Some("rg"))]);
323+
324+
let merged = resolve(role, Some(role_group));
325+
326+
assert_eq!(
327+
merged,
328+
BTreeMap::from([
329+
("a".to_string(), "rg".to_string()), // role-group wins for shared keys
330+
("b".to_string(), "role-only".to_string()), // role-only keys are kept
331+
])
332+
);
333+
}
334+
335+
/// Illustrates the key consequence of using `Merge` (rather than `.extend()`, as `main`'s
336+
/// product-config did): a role-group `null` is treated as "inherit", so the role-level value
337+
/// is *kept* — it does NOT unset the key. Under the old `.extend()` behaviour this same input
338+
/// would have removed `a` entirely.
339+
#[test]
340+
fn role_group_null_inherits_role_value_rather_than_unsetting_it() {
341+
let role = overrides(&[("a", Some("role"))]);
342+
let role_group = overrides(&[("a", None)]); // explicit `null` at the more specific level
343+
344+
let merged = resolve(role, Some(role_group));
345+
346+
assert_eq!(
347+
merged,
348+
BTreeMap::from([("a".to_string(), "role".to_string())]),
349+
"a role-group `null` should inherit the role-level value under Merge semantics"
350+
);
351+
}
352+
353+
#[test]
354+
fn without_a_role_group_role_values_are_kept_and_nulls_dropped() {
355+
let role = overrides(&[("a", Some("role")), ("b", None)]);
356+
357+
let merged = resolve(role, None);
358+
359+
assert_eq!(
360+
merged,
361+
BTreeMap::from([("a".to_string(), "role".to_string())])
362+
);
363+
}
364+
}

0 commit comments

Comments
 (0)