feat: config overrides for structured config files#1177
feat: config overrides for structured config files#1177
Conversation
sbernauer
left a comment
There was a problem hiding this comment.
Only small stuff noticed during daily, real review will follow
b0e1dc8 to
8d9c6cd
Compare
| ]); | ||
|
|
||
| let result = overrides.apply(&base); | ||
| assert!(result.is_err(), "removing a nonexistent path should fail"); |
There was a problem hiding this comment.
| assert!(result.is_err(), "removing a nonexistent path should fail"); | |
| assert!( | |
| matches!(result.unwrap_err(), Error::ApplyJsonPatch { source } if source.to_string() | |
| == "operation '/0' failed at path '/nonexistent': path is invalid"), | |
| "removing a nonexistent path should fail" | |
| ); |
| assert!( | ||
| result.is_err(), | ||
| "invalid patch operation should return an error" | ||
| ); |
There was a problem hiding this comment.
| assert!( | |
| result.is_err(), | |
| "invalid patch operation should return an error" | |
| ); | |
| assert!( | |
| matches!(result.unwrap_err(), Error::DeserializeJsonPatchOperation { source, index: 0 } if source.to_string() | |
| == "missing field `op` at line 1 column 19"), | |
| "invalid patch operation should return an error" | |
| ); |
| let overrides = JsonConfigOverrides::UserProvided("not valid json".to_owned()); | ||
|
|
||
| let result = overrides.apply(&base); | ||
| assert!(result.is_err(), "invalid JSON should return an error"); |
There was a problem hiding this comment.
| assert!(result.is_err(), "invalid JSON should return an error"); | |
| assert!( | |
| matches!(result.unwrap_err(), Error::ParseUserProvidedJson { source } if source.to_string() | |
| == "expected ident at line 1 column 2"), | |
| "invalid JSON should return an error" | |
| ); |
| T, | ||
| U = GenericRoleConfig, |
There was a problem hiding this comment.
nit: It would be great if we could rename all T and U to Config and RoleConfg accordingly.
| @@ -302,32 +311,36 @@ pub struct Role< | |||
| T, | |||
| U = GenericRoleConfig, | |||
| ProductSpecificCommonConfig = GenericProductSpecificCommonConfig, | |||
There was a problem hiding this comment.
nit: It would be great to rename ProductSpecificCommonConfig to CommonConfig for consitency
crates/xtask/src/crd/dummy.rs
Outdated
| #[serde(default)] | ||
| pub object_overrides: ObjectOverrides, | ||
|
|
||
| json_config_overrides: Option<stackable_operator::config_overrides::JsonConfigOverrides>, |
There was a problem hiding this comment.
Please use this struct instead (this fields can than be removed)
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(crate = "stackable_operator::schemars")]
pub struct ProductConfigOverrides {
#[serde(rename = "my.json")]
my_json: Option<JsonConfigOverrides>,
}
There was a problem hiding this comment.
(this is also the commit that switches from backwards-compatible config overrides to making it break for existing operators, so they are forced to opt in to the new config overrides)
88710df to
abf16cd
Compare
abf16cd to
20e94e3
Compare
20e94e3 to
4b84601
Compare
Description
Implementation of https://github.com/stackabletech/decisions/issues/73, needed for stackabletech/opa-operator#756
Problem
The existing
configOverridesmechanism usesHashMap<String, HashMap<String, String>>(filename → flat key-value pairs). This works well for flat formats like.propertiesfiles and Hadoop XML, but it cannot express modifications to nested or structured formats like JSON. For example, there is no clean way to overridemin_delay_secondsinside a deeply nested OPAconfig.json.Solution
This PR adds strategy-based
configOverridesbuilding blocks tooperator-rs. Instead of a single flat map, operators can now compose typed override structs that choose a patch strategy per config file. The CRD schema explicitly encodes which strategies are supported for which files, which means invalid input is rejected by the Kubernetes API server before the operator ever sees it.The general architecture for config overrides is: operator-rs handles merging (combining base config with user overrides), while the operator handles rendering (turning the merged result into the actual file content). For key-value files, the merging happens inside the existing product config pipeline. For structured files like JSON, the merging happens via e.g.
JsonConfigOverrides::apply(). Both are operator-rs code. For rendering, the operator picks the right format: sometimes using shared helpers from operator-rs (e.g. properties file writers), sometimes doing it directly (e.g.serde_json::to_string_pretty).Breaking changes to
CommonConfiguration,Role, andRoleGroupThese structs gained a new required
ConfigOverridestype parameter. There is no default, which means every operator must explicitly specify itsConfigOverridestype. This is a breaking change: all existing operators need to define their own config overrides struct and pass it as a type parameter.The type parameters were also renamed for clarity:
TtoConfig,UtoRoleConfig, andProductSpecificCommonConfigtoCommonConfig.New
config_overridesmoduleIt contains:
New patch strategies for JSON files:
JsonConfigOverrides: enum supporting three strategies for JSON config files:jsonMergePatch: RFC 7396, simple nested overrides expressed as YAML/JSONjsonPatches: RFC 6902, fine-grained operations (add, remove, replace, move, test)userProvided: full file replacement escape hatchTyped key-value overrides:
KeyValueConfigOverrides: typed wrapper for flat key-value files (.properties, Hadoop XML).Uses
#[serde(flatten)]so it serializes identically to the oldHashMap<String, String>.KeyValueOverridesProvidertrait:ConfigOverridestype. The default implementation returns an empty map, so operators that only use structured overrides (like OPA) don't need any custom logic.The shared product config pipeline (
transform_all_roles_to_config) processesPropertyNameKind::Fileentries by callingget_key_value_overrideson theConfigOverridestype. Rust requires this trait bound at compile time even if a particular operator never passesPropertyNameKind::Fileentries. Operators that only use structured overrides (like OPA withJsonConfigOverrides) can rely on the default no-op implementation. Every operator's config overrides type must implement this trait, butimpl KeyValueOverridesProvider for OpaConfigOverrides {}(using the default no-op) is sufficient for operators that don't use key-value overrides.Example for a hypothetical NiFi with the new typed overrides, which uses
KeyValueConfigOverrides:Why a generic type parameter instead of an enum?
An enum containing all strategies would mean every operator advertises support for every strategy on every file. The current approach (operators compose a struct from building blocks) lets the CRD schema precisely reflect what is actually supported. Invalid combinations are rejected at admission time, not at runtime.
What is NOT included
XmlConfigOverrides/ XML patch strategy (RFC 5261): will be added when needed (e.g. for NiFi).Definition of Done Checklist
Author
Reviewer
Acceptance