From 4d18a9a14e15ed0125d4d33fff001b2e2b12f59f Mon Sep 17 00:00:00 2001 From: dervoeti Date: Tue, 17 Mar 2026 21:53:32 +0100 Subject: [PATCH 1/9] feat: config overrides for structured config files --- .../src/config_overrides.rs | 331 ++++++++++++++++++ crates/stackable-operator/src/lib.rs | 1 + .../src/product_config_utils.rs | 40 +-- crates/stackable-operator/src/role_utils.rs | 58 ++- 4 files changed, 393 insertions(+), 37 deletions(-) create mode 100644 crates/stackable-operator/src/config_overrides.rs diff --git a/crates/stackable-operator/src/config_overrides.rs b/crates/stackable-operator/src/config_overrides.rs new file mode 100644 index 000000000..e70932e69 --- /dev/null +++ b/crates/stackable-operator/src/config_overrides.rs @@ -0,0 +1,331 @@ +//! Building-block types for strategy-based `configOverrides`. +//! +//! Operators declare typed override structs choosing patch strategies per file +//! (e.g. [`JsonConfigOverrides`] for JSON files, [`KeyValueConfigOverrides`] for +//! properties files). The types here are composed by each operator into its +//! CRD-specific `configOverrides` struct. + +use std::collections::{BTreeMap, HashMap}; + +use schemars::{JsonSchema, Schema, json_schema}; +use serde::{Deserialize, Serialize}; +use snafu::{ResultExt, Snafu}; + +/// Generates a JSON schema that accepts any JSON value. +/// +/// Kubernetes CRDs do not support the `true` schema shorthand that +/// `serde_json::Value` generates by default. Instead we emit a schema +/// with `x-kubernetes-preserve-unknown-fields: true` which tells the +/// API server to store the value as-is. +fn arbitrary_json_value(_gen: &mut schemars::generate::SchemaGenerator) -> Schema { + json_schema!({ + "x-kubernetes-preserve-unknown-fields": true, + }) +} + +/// Generates a JSON schema for a list of JSON patch operation strings (RFC 6902). +fn json_patch_string_list(_gen: &mut schemars::generate::SchemaGenerator) -> Schema { + json_schema!({ + "type": "array", + "items": { + "type": "string", + }, + }) +} + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to serialize base document to JSON"))] + SerializeBaseDocument { source: serde_json::Error }, + + #[snafu(display("failed to apply JSON patch (RFC 6902)"))] + ApplyJsonPatch { source: json_patch::PatchError }, + + #[snafu(display("failed to deserialize JSON patch operation {index} from string"))] + DeserializeJsonPatchOperation { + source: serde_json::Error, + index: usize, + }, + + #[snafu(display("failed to parse user-provided JSON content"))] + ParseUserProvidedJson { source: serde_json::Error }, +} + +/// Trait that allows the product config pipeline to extract flat key-value +/// overrides from any `configOverrides` type. +/// +/// The default `HashMap>` implements this +/// by looking up the file name and returning its entries. Typed override +/// structs that have no key-value files can use the default implementation, +/// which returns an empty map. +pub trait KeyValueOverridesProvider { + fn get_key_value_overrides(&self, _file: &str) -> BTreeMap> { + BTreeMap::new() + } +} + +impl KeyValueOverridesProvider + for HashMap> +{ + fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { + self.get(file) + .map(|entries| { + entries + .iter() + .map(|(k, v)| (k.clone(), Some(v.clone()))) + .collect() + }) + .unwrap_or_default() + } +} + +/// Flat key-value overrides for `*.properties`, Hadoop XML, etc. +/// +/// This is backwards-compatible with the existing flat key-value YAML format +/// used by `HashMap`. +#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +pub struct KeyValueConfigOverrides { + #[serde(flatten)] + pub overrides: BTreeMap, +} + +impl KeyValueConfigOverrides { + /// Returns the overrides as a `BTreeMap>`, matching + /// the format expected by the product config pipeline. + /// + /// This is useful when implementing [`KeyValueOverridesProvider`] for a + /// typed override struct that contains [`KeyValueConfigOverrides`] fields. + pub fn as_overrides(&self) -> BTreeMap> { + self.overrides + .iter() + .map(|(k, v)| (k.clone(), Some(v.clone()))) + .collect() + } +} + +/// ConfigOverrides that can be applied to a JSON file. +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum JsonConfigOverrides { + /// Can be set to arbitrary YAML content, which is converted to JSON and used as + /// [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) JSON merge patch. + #[schemars(schema_with = "arbitrary_json_value")] + JsonMergePatch(serde_json::Value), + + /// List of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON patches. + /// + /// Can be used when more flexibility is needed, e.g. to only modify elements + /// in a list based on a condition. + /// + /// A patch looks something like + /// + /// `{"op": "test", "path": "/0/name", "value": "Andrew"}` + /// + /// or + /// + /// `{"op": "add", "path": "/0/happy", "value": true}` + #[schemars(schema_with = "json_patch_string_list")] + JsonPatches(Vec), + + /// Override the entire config file with the specified String. + UserProvided(String), +} + +impl JsonConfigOverrides { + /// Applies this override to a base JSON document and returns the patched + /// document as a [`serde_json::Value`]. + /// + /// For [`JsonConfigOverrides::JsonMergePatch`] and + /// [`JsonConfigOverrides::JsonPatches`], the base document is patched + /// according to the respective RFC. + /// + /// For [`JsonConfigOverrides::UserProvided`], the base document is ignored + /// entirely and the user-provided string is parsed and returned. + pub fn apply(&self, base: &serde_json::Value) -> Result { + match self { + JsonConfigOverrides::JsonMergePatch(patch) => { + let mut doc = base.clone(); + json_patch::merge(&mut doc, patch); + Ok(doc) + } + JsonConfigOverrides::JsonPatches(patches) => { + let mut doc = base.clone(); + let operations: Vec = patches + .iter() + .enumerate() + .map(|(index, patch_str)| { + serde_json::from_str(patch_str).context( + DeserializeJsonPatchOperationSnafu { index }, + ) + }) + .collect::, _>>()?; + json_patch::patch(&mut doc, &operations).context(ApplyJsonPatchSnafu)?; + Ok(doc) + } + JsonConfigOverrides::UserProvided(content) => { + serde_json::from_str(content).context(ParseUserProvidedJsonSnafu) + } + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use serde_json::json; + + use super::*; + + #[test] + fn json_merge_patch_add_and_overwrite_fields() { + let base = json!({ + "bundles": { + "authz": { + "polling": { + "min_delay_seconds": 10, + "max_delay_seconds": 20 + } + } + } + }); + + let overrides = JsonConfigOverrides::JsonMergePatch(json!({ + "bundles": { + "authz": { + "polling": { + "min_delay_seconds": 3, + "max_delay_seconds": 5 + } + } + }, + "default_decision": "/http/example/authz/allow" + })); + + let result = overrides.apply(&base).expect("merge patch should succeed"); + + assert_eq!(result["bundles"]["authz"]["polling"]["min_delay_seconds"], 3); + assert_eq!(result["bundles"]["authz"]["polling"]["max_delay_seconds"], 5); + assert_eq!( + result["default_decision"], + "/http/example/authz/allow" + ); + } + + #[test] + fn json_merge_patch_remove_field_with_null() { + let base = json!({ + "keep": "this", + "remove": "this" + }); + + let overrides = JsonConfigOverrides::JsonMergePatch(json!({ + "remove": null + })); + + let result = overrides.apply(&base).expect("merge patch should succeed"); + + assert_eq!(result["keep"], "this"); + assert!(result.get("remove").is_none()); + } + + #[test] + fn json_patch_add_remove_replace() { + let base = json!({ + "foo": "bar", + "baz": "qux" + }); + + let overrides = JsonConfigOverrides::JsonPatches(vec![ + r#"{"op": "replace", "path": "/foo", "value": "replaced"}"#.to_owned(), + r#"{"op": "remove", "path": "/baz"}"#.to_owned(), + r#"{"op": "add", "path": "/new_key", "value": "new_value"}"#.to_owned(), + ]); + + let result = overrides.apply(&base).expect("JSON patch should succeed"); + + assert_eq!(result["foo"], "replaced"); + assert!(result.get("baz").is_none()); + assert_eq!(result["new_key"], "new_value"); + } + + #[test] + fn json_patch_invalid_path_returns_error() { + let base = json!({"foo": "bar"}); + + let overrides = JsonConfigOverrides::JsonPatches(vec![ + r#"{"op": "remove", "path": "/nonexistent"}"#.to_owned(), + ]); + + let result = overrides.apply(&base); + assert!(result.is_err(), "removing a nonexistent path should fail"); + } + + #[test] + fn json_patch_invalid_operation_returns_error() { + let base = json!({"foo": "bar"}); + + let overrides = JsonConfigOverrides::JsonPatches(vec![ + r#"{"not_an_op": true}"#.to_owned(), + ]); + + let result = overrides.apply(&base); + assert!( + result.is_err(), + "invalid patch operation should return an error" + ); + } + + #[test] + fn user_provided_ignores_base() { + let base = json!({"foo": "bar"}); + let content = "{\"custom\": true}"; + + let overrides = JsonConfigOverrides::UserProvided(content.to_owned()); + + let result = overrides.apply(&base).expect("user provided should succeed"); + assert_eq!(result, json!({"custom": true})); + } + + #[test] + fn user_provided_invalid_json_returns_error() { + let base = json!({"foo": "bar"}); + + let overrides = JsonConfigOverrides::UserProvided("not valid json".to_owned()); + + let result = overrides.apply(&base); + assert!(result.is_err(), "invalid JSON should return an error"); + } + + #[test] + fn key_value_config_overrides_as_overrides() { + let mut overrides = BTreeMap::new(); + overrides.insert("key1".to_owned(), "value1".to_owned()); + overrides.insert("key2".to_owned(), "value2".to_owned()); + + let kv = KeyValueConfigOverrides { overrides }; + let result = kv.as_overrides(); + + assert_eq!(result.len(), 2); + assert_eq!(result.get("key1"), Some(&Some("value1".to_owned()))); + assert_eq!(result.get("key2"), Some(&Some("value2".to_owned()))); + } + + #[test] + fn key_value_overrides_provider_for_hashmap() { + let mut config_overrides = + HashMap::>::new(); + let mut file_overrides = HashMap::new(); + file_overrides.insert("key1".to_owned(), "value1".to_owned()); + file_overrides.insert("key2".to_owned(), "value2".to_owned()); + config_overrides.insert("myfile.properties".to_owned(), file_overrides); + + let result = config_overrides.get_key_value_overrides("myfile.properties"); + assert_eq!(result.len(), 2); + assert_eq!(result.get("key1"), Some(&Some("value1".to_owned()))); + assert_eq!(result.get("key2"), Some(&Some("value2".to_owned()))); + + let empty = config_overrides.get_key_value_overrides("nonexistent.properties"); + assert!(empty.is_empty()); + } +} diff --git a/crates/stackable-operator/src/lib.rs b/crates/stackable-operator/src/lib.rs index 46616252c..1e0fe620c 100644 --- a/crates/stackable-operator/src/lib.rs +++ b/crates/stackable-operator/src/lib.rs @@ -12,6 +12,7 @@ pub mod client; pub mod cluster_resources; pub mod commons; pub mod config; +pub mod config_overrides; pub mod constants; pub mod cpu; #[cfg(feature = "crds")] diff --git a/crates/stackable-operator/src/product_config_utils.rs b/crates/stackable-operator/src/product_config_utils.rs index bcf75572e..94614d9d0 100644 --- a/crates/stackable-operator/src/product_config_utils.rs +++ b/crates/stackable-operator/src/product_config_utils.rs @@ -7,7 +7,10 @@ use serde::Serialize; use snafu::{ResultExt, Snafu}; use tracing::{debug, error, warn}; -use crate::role_utils::{CommonConfiguration, Role}; +use crate::{ + config_overrides::KeyValueOverridesProvider, + role_utils::{CommonConfiguration, Role}, +}; pub const CONFIG_OVERRIDE_FILE_HEADER_KEY: &str = "EXPERIMENTAL_FILE_HEADER"; pub const CONFIG_OVERRIDE_FILE_FOOTER_KEY: &str = "EXPERIMENTAL_FILE_FOOTER"; @@ -171,13 +174,13 @@ pub fn config_for_role_and_group<'a>( /// - `roles`: A map keyed by role names. The value is a tuple of a vector of `PropertyNameKind` /// like (Cli, Env or Files) and [`crate::role_utils::Role`] with a boxed [`Configuration`]. #[allow(clippy::type_complexity)] -pub fn transform_all_roles_to_config( +pub fn transform_all_roles_to_config( resource: &T::Configurable, roles: HashMap< String, ( Vec, - Role, + Role, ), >, ) -> Result @@ -185,6 +188,7 @@ where T: Configuration, U: Default + JsonSchema + Serialize, ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + ConfigOverrides: Default + JsonSchema + Serialize + KeyValueOverridesProvider, { let mut result = HashMap::new(); @@ -380,16 +384,17 @@ fn process_validation_result( /// - `role_name` - The name of the role. /// - `role` - The role for which to transform the configuration parameters. /// - `property_kinds` - Used as "buckets" to partition the configuration properties by. -fn transform_role_to_config( +fn transform_role_to_config( resource: &T::Configurable, role_name: &str, - role: &Role, + role: &Role, property_kinds: &[PropertyNameKind], ) -> Result where T: Configuration, U: Default + JsonSchema + Serialize, ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + ConfigOverrides: Default + JsonSchema + Serialize + KeyValueOverridesProvider, { let mut result = HashMap::new(); @@ -444,10 +449,10 @@ where /// - `role_name` - Not used directly but passed on to the `Configuration::compute_*` calls. /// - `config` - The configuration properties to partition. /// - `property_kinds` - The "buckets" used to partition the configuration properties. -fn parse_role_config( +fn parse_role_config( resource: &::Configurable, role_name: &str, - config: &CommonConfiguration, + config: &CommonConfiguration, property_kinds: &[PropertyNameKind], ) -> Result>>> where @@ -474,12 +479,13 @@ where Ok(result) } -fn parse_role_overrides( - config: &CommonConfiguration, +fn parse_role_overrides( + config: &CommonConfiguration, property_kinds: &[PropertyNameKind], ) -> Result>>> where T: Configuration, + ConfigOverrides: KeyValueOverridesProvider, { let mut result = HashMap::new(); for property_kind in property_kinds { @@ -511,23 +517,15 @@ where Ok(result) } -fn parse_file_overrides( - config: &CommonConfiguration, +fn parse_file_overrides( + config: &CommonConfiguration, file: &str, ) -> Result>> where T: Configuration, + ConfigOverrides: KeyValueOverridesProvider, { - let mut final_overrides: BTreeMap> = BTreeMap::new(); - - // For Conf files only process overrides that match our file name - if let Some(config) = config.config_overrides.get(file) { - for (key, value) in config { - final_overrides.insert(key.clone(), Some(value.clone())); - } - } - - Ok(final_overrides) + Ok(config.config_overrides.get_key_value_overrides(file)) } /// Extract the environment variables of a rolegroup config into a vector of EnvVars. diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index b82bd6623..08cda871d 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -118,10 +118,15 @@ pub enum Error { #[serde( rename_all = "camelCase", bound( - deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>" + deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] -pub struct CommonConfiguration { +#[schemars(bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema")] +pub struct CommonConfiguration< + T, + ProductSpecificCommonConfig, + ConfigOverrides = HashMap>, +> { #[serde(default)] // We can't depend on T being `Default`, since that trait is not object-safe // We only need to generate schemas for fully specified types, but schemars_derive @@ -135,7 +140,7 @@ pub struct CommonConfiguration { /// and consult the operator specific usage guide documentation for details on the /// available config files and settings for the specific product. #[serde(default)] - pub config_overrides: HashMap>, + pub config_overrides: ConfigOverrides, /// `envOverrides` configure environment variables to be set in the Pods. /// It is a map from strings to strings - environment variables and the value to set. @@ -171,7 +176,9 @@ pub struct CommonConfiguration { pub product_specific_common_config: ProductSpecificCommonConfig, } -impl CommonConfiguration { +impl + CommonConfiguration +{ fn default_config() -> serde_json::Value { serde_json::json!({}) } @@ -302,32 +309,37 @@ pub struct Role< T, U = GenericRoleConfig, ProductSpecificCommonConfig = GenericProductSpecificCommonConfig, + ConfigOverrides = HashMap>, > where // Don't remove this trait bounds!!! // We don't know why, but if you remove either of them, the generated default value in the CRDs will // be missing! U: Default + JsonSchema + Serialize, ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + ConfigOverrides: Default + JsonSchema + Serialize, { #[serde( flatten, bound( - deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Deserialize<'de>" + deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Deserialize<'de>, ConfigOverrides: Deserialize<'de>" ) )] - pub config: CommonConfiguration, + pub config: CommonConfiguration, #[serde(default)] pub role_config: U, - pub role_groups: HashMap>, + pub role_groups: + HashMap>, } -impl Role +impl + Role where T: Configuration + 'static, U: Default + JsonSchema + Serialize, ProductSpecificCommonConfig: Default + JsonSchema + Serialize + Clone, + ConfigOverrides: Default + JsonSchema + Serialize, { /// This casts a generic struct implementing [`crate::product_config_utils::Configuration`] /// and used in [`Role`] into a Box of a dynamically dispatched @@ -336,8 +348,12 @@ where /// have different structs implementing Configuration. pub fn erase( self, - ) -> Role>, U, ProductSpecificCommonConfig> - { + ) -> Role< + Box>, + U, + ProductSpecificCommonConfig, + ConfigOverrides, + > { Role { config: CommonConfiguration { config: Box::new(self.config.config) @@ -376,9 +392,10 @@ where } } -impl Role +impl Role where U: Default + JsonSchema + Serialize, + ConfigOverrides: Default + JsonSchema + Serialize, { /// Merges jvm argument overrides from /// @@ -431,25 +448,34 @@ pub struct EmptyRoleConfig {} #[serde( rename_all = "camelCase", bound( - deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>" + deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] -pub struct RoleGroup { +#[schemars(bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema")] +pub struct RoleGroup< + T, + ProductSpecificCommonConfig, + ConfigOverrides = HashMap>, +> { #[serde(flatten)] - pub config: CommonConfiguration, + pub config: CommonConfiguration, pub replicas: Option, } -impl RoleGroup { +impl + RoleGroup +{ pub fn validate_config( &self, - role: &Role, + role: &Role, default_config: &T, ) -> Result where C: FromFragment, T: Merge + Clone, U: Default + JsonSchema + Serialize, + ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + ConfigOverrides: Default + JsonSchema + Serialize, { let mut role_config = role.config.config.clone(); role_config.merge(default_config); From b3a92e729863730a31d74d96fb8a21bf8d7f3bca Mon Sep 17 00:00:00 2001 From: dervoeti Date: Wed, 18 Mar 2026 20:15:50 +0000 Subject: [PATCH 2/9] chore: regenerate CRD preview --- crates/stackable-operator/crds/DummyCluster.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index 18b0000f3..eb033d37b 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -717,7 +717,6 @@ spec: additionalProperties: type: string type: object - default: {} description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the @@ -1329,7 +1328,6 @@ spec: additionalProperties: type: string type: object - default: {} description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the From e10c6db467bea204e9271b15374b4dcf08c0078d Mon Sep 17 00:00:00 2001 From: dervoeti Date: Thu, 19 Mar 2026 08:03:42 +0000 Subject: [PATCH 3/9] chore: apply nightly rustfmt formatting --- .../src/config_overrides.rs | 31 +++++++++---------- crates/stackable-operator/src/role_utils.rs | 11 ++++--- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/crates/stackable-operator/src/config_overrides.rs b/crates/stackable-operator/src/config_overrides.rs index e70932e69..694ab83fe 100644 --- a/crates/stackable-operator/src/config_overrides.rs +++ b/crates/stackable-operator/src/config_overrides.rs @@ -64,9 +64,7 @@ pub trait KeyValueOverridesProvider { } } -impl KeyValueOverridesProvider - for HashMap> -{ +impl KeyValueOverridesProvider for HashMap> { fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { self.get(file) .map(|entries| { @@ -154,9 +152,8 @@ impl JsonConfigOverrides { .iter() .enumerate() .map(|(index, patch_str)| { - serde_json::from_str(patch_str).context( - DeserializeJsonPatchOperationSnafu { index }, - ) + serde_json::from_str(patch_str) + .context(DeserializeJsonPatchOperationSnafu { index }) }) .collect::, _>>()?; json_patch::patch(&mut doc, &operations).context(ApplyJsonPatchSnafu)?; @@ -204,12 +201,15 @@ mod tests { let result = overrides.apply(&base).expect("merge patch should succeed"); - assert_eq!(result["bundles"]["authz"]["polling"]["min_delay_seconds"], 3); - assert_eq!(result["bundles"]["authz"]["polling"]["max_delay_seconds"], 5); assert_eq!( - result["default_decision"], - "/http/example/authz/allow" + result["bundles"]["authz"]["polling"]["min_delay_seconds"], + 3 ); + assert_eq!( + result["bundles"]["authz"]["polling"]["max_delay_seconds"], + 5 + ); + assert_eq!(result["default_decision"], "/http/example/authz/allow"); } #[test] @@ -265,9 +265,7 @@ mod tests { fn json_patch_invalid_operation_returns_error() { let base = json!({"foo": "bar"}); - let overrides = JsonConfigOverrides::JsonPatches(vec![ - r#"{"not_an_op": true}"#.to_owned(), - ]); + let overrides = JsonConfigOverrides::JsonPatches(vec![r#"{"not_an_op": true}"#.to_owned()]); let result = overrides.apply(&base); assert!( @@ -283,7 +281,9 @@ mod tests { let overrides = JsonConfigOverrides::UserProvided(content.to_owned()); - let result = overrides.apply(&base).expect("user provided should succeed"); + let result = overrides + .apply(&base) + .expect("user provided should succeed"); assert_eq!(result, json!({"custom": true})); } @@ -313,8 +313,7 @@ mod tests { #[test] fn key_value_overrides_provider_for_hashmap() { - let mut config_overrides = - HashMap::>::new(); + let mut config_overrides = HashMap::>::new(); let mut file_overrides = HashMap::new(); file_overrides.insert("key1".to_owned(), "value1".to_owned()); file_overrides.insert("key2".to_owned(), "value2".to_owned()); diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 08cda871d..747af2334 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -121,7 +121,9 @@ pub enum Error { deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] -#[schemars(bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema")] +#[schemars( + bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" +)] pub struct CommonConfiguration< T, ProductSpecificCommonConfig, @@ -329,8 +331,7 @@ pub struct Role< #[serde(default)] pub role_config: U, - pub role_groups: - HashMap>, + pub role_groups: HashMap>, } impl @@ -451,7 +452,9 @@ pub struct EmptyRoleConfig {} deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] -#[schemars(bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema")] +#[schemars( + bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" +)] pub struct RoleGroup< T, ProductSpecificCommonConfig, From 68f58372a23fe8b0ba16a1d07b8fa6d542825eb0 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Thu, 19 Mar 2026 14:27:01 +0100 Subject: [PATCH 4/9] fix: addressed first round of review feedback --- .../stackable-operator/crds/DummyCluster.yaml | 47 +++++++++++++++++++ .../src/config_overrides.rs | 27 ++--------- crates/xtask/src/crd/dummy.rs | 4 ++ 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index eb033d37b..58f7b8a4d 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -164,9 +164,56 @@ spec: type: object hostName: type: string + jsonConfigOverrides: + nullable: true + oneOf: + - required: + - jsonMergePatch + - required: + - jsonPatches + - required: + - userProvided + properties: + jsonMergePatch: + description: |- + Can be set to arbitrary YAML content, which is converted to JSON and used as + [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) JSON merge patch. + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPatches: + description: |- + List of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON patches. + + Can be used when more flexibility is needed, e.g. to only modify elements + in a list based on a condition. + + A patch looks something like + + `{"op": "test", "path": "/0/name", "value": "Andrew"}` + + or + + `{"op": "add", "path": "/0/happy", "value": true}` + items: + type: string + type: array + userProvided: + description: Override the entire config file with the specified String. + type: string + type: object kerberosRealmName: description: A validated kerberos realm name type, for use in CRDs. type: string + keyValueConfigOverrides: + additionalProperties: + type: string + description: |- + Flat key-value overrides for `*.properties`, Hadoop XML, etc. + + This is backwards-compatible with the existing flat key-value YAML format + used by `HashMap`. + nullable: true + type: object nodes: description: |- This struct represents a role - e.g. HDFS datanodes or Trino workers. It has a key-value-map containing diff --git a/crates/stackable-operator/src/config_overrides.rs b/crates/stackable-operator/src/config_overrides.rs index 694ab83fe..7405abff6 100644 --- a/crates/stackable-operator/src/config_overrides.rs +++ b/crates/stackable-operator/src/config_overrides.rs @@ -7,31 +7,11 @@ use std::collections::{BTreeMap, HashMap}; -use schemars::{JsonSchema, Schema, json_schema}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Snafu}; -/// Generates a JSON schema that accepts any JSON value. -/// -/// Kubernetes CRDs do not support the `true` schema shorthand that -/// `serde_json::Value` generates by default. Instead we emit a schema -/// with `x-kubernetes-preserve-unknown-fields: true` which tells the -/// API server to store the value as-is. -fn arbitrary_json_value(_gen: &mut schemars::generate::SchemaGenerator) -> Schema { - json_schema!({ - "x-kubernetes-preserve-unknown-fields": true, - }) -} - -/// Generates a JSON schema for a list of JSON patch operation strings (RFC 6902). -fn json_patch_string_list(_gen: &mut schemars::generate::SchemaGenerator) -> Schema { - json_schema!({ - "type": "array", - "items": { - "type": "string", - }, - }) -} +use crate::utils::crds::raw_object_schema; #[derive(Debug, Snafu)] pub enum Error { @@ -107,7 +87,7 @@ impl KeyValueConfigOverrides { pub enum JsonConfigOverrides { /// Can be set to arbitrary YAML content, which is converted to JSON and used as /// [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) JSON merge patch. - #[schemars(schema_with = "arbitrary_json_value")] + #[schemars(schema_with = "raw_object_schema")] JsonMergePatch(serde_json::Value), /// List of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON patches. @@ -122,7 +102,6 @@ pub enum JsonConfigOverrides { /// or /// /// `{"op": "add", "path": "/0/happy", "value": true}` - #[schemars(schema_with = "json_patch_string_list")] JsonPatches(Vec), /// Override the entire config file with the specified String. diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index 28ed0581c..917e534f3 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -54,6 +54,10 @@ pub mod versioned { #[serde(default)] pub object_overrides: ObjectOverrides, + json_config_overrides: Option, + key_value_config_overrides: + Option, + // Already versioned client_authentication_details: stackable_operator::crd::authentication::core::v1alpha1::ClientAuthenticationDetails, From a839d7e795560cbee617efea1d34ec1eb873d4e6 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Fri, 20 Mar 2026 21:04:05 +0000 Subject: [PATCH 5/9] refactor!: remove HashMap default for ConfigOverrides type parameter --- .../stackable-operator/crds/DummyCluster.yaml | 198 +++++++++++++----- .../src/commons/resources.rs | 5 +- .../src/config_overrides.rs | 39 +--- .../src/product_config_utils.rs | 91 ++++---- crates/stackable-operator/src/role_utils.rs | 31 ++- crates/xtask/src/crd/dummy.rs | 44 +++- 6 files changed, 258 insertions(+), 150 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index 58f7b8a4d..1439afe2b 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -82,6 +82,59 @@ spec: and `stopped` will take no effect until `reconciliationPaused` is set to false or removed. type: boolean type: object + configOverrides: + default: {} + description: |- + Typed config override strategies for Dummy config files. + + Demonstrates both JSON-formatted (`config.json`) and key-value-formatted + (`dummy.properties`) config file overrides. + properties: + config.json: + description: Overrides for the `config.json` file. + nullable: true + oneOf: + - required: + - jsonMergePatch + - required: + - jsonPatches + - required: + - userProvided + properties: + jsonMergePatch: + description: |- + Can be set to arbitrary YAML content, which is converted to JSON and used as + [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) JSON merge patch. + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPatches: + description: |- + List of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON patches. + + Can be used when more flexibility is needed, e.g. to only modify elements + in a list based on a condition. + + A patch looks something like + + `{"op": "test", "path": "/0/name", "value": "Andrew"}` + + or + + `{"op": "add", "path": "/0/happy", "value": true}` + items: + type: string + type: array + userProvided: + description: Override the entire config file with the specified String. + type: string + type: object + dummy.properties: + additionalProperties: + type: string + description: Overrides for the `dummy.properties` file. + nullable: true + type: object + type: object domainName: description: A validated domain name type conforming to RFC 1123, so e.g. not an IP address type: string @@ -164,56 +217,9 @@ spec: type: object hostName: type: string - jsonConfigOverrides: - nullable: true - oneOf: - - required: - - jsonMergePatch - - required: - - jsonPatches - - required: - - userProvided - properties: - jsonMergePatch: - description: |- - Can be set to arbitrary YAML content, which is converted to JSON and used as - [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) JSON merge patch. - type: object - x-kubernetes-preserve-unknown-fields: true - jsonPatches: - description: |- - List of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON patches. - - Can be used when more flexibility is needed, e.g. to only modify elements - in a list based on a condition. - - A patch looks something like - - `{"op": "test", "path": "/0/name", "value": "Andrew"}` - - or - - `{"op": "add", "path": "/0/happy", "value": true}` - items: - type: string - type: array - userProvided: - description: Override the entire config file with the specified String. - type: string - type: object kerberosRealmName: description: A validated kerberos realm name type, for use in CRDs. type: string - keyValueConfigOverrides: - additionalProperties: - type: string - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. - nullable: true - type: object nodes: description: |- This struct represents a role - e.g. HDFS datanodes or Trino workers. It has a key-value-map containing @@ -760,16 +766,57 @@ spec: type: object type: object configOverrides: - additionalProperties: - additionalProperties: - type: string - type: object description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. + properties: + config.json: + description: Overrides for the `config.json` file. + nullable: true + oneOf: + - required: + - jsonMergePatch + - required: + - jsonPatches + - required: + - userProvided + properties: + jsonMergePatch: + description: |- + Can be set to arbitrary YAML content, which is converted to JSON and used as + [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) JSON merge patch. + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPatches: + description: |- + List of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON patches. + + Can be used when more flexibility is needed, e.g. to only modify elements + in a list based on a condition. + + A patch looks something like + + `{"op": "test", "path": "/0/name", "value": "Andrew"}` + + or + + `{"op": "add", "path": "/0/happy", "value": true}` + items: + type: string + type: array + userProvided: + description: Override the entire config file with the specified String. + type: string + type: object + dummy.properties: + additionalProperties: + type: string + description: Overrides for the `dummy.properties` file. + nullable: true + type: object type: object envOverrides: additionalProperties: @@ -1371,16 +1418,57 @@ spec: type: object type: object configOverrides: - additionalProperties: - additionalProperties: - type: string - type: object description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. + properties: + config.json: + description: Overrides for the `config.json` file. + nullable: true + oneOf: + - required: + - jsonMergePatch + - required: + - jsonPatches + - required: + - userProvided + properties: + jsonMergePatch: + description: |- + Can be set to arbitrary YAML content, which is converted to JSON and used as + [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) JSON merge patch. + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPatches: + description: |- + List of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON patches. + + Can be used when more flexibility is needed, e.g. to only modify elements + in a list based on a condition. + + A patch looks something like + + `{"op": "test", "path": "/0/name", "value": "Andrew"}` + + or + + `{"op": "add", "path": "/0/happy", "value": true}` + items: + type: string + type: array + userProvided: + description: Override the entire config file with the specified String. + type: string + type: object + dummy.properties: + additionalProperties: + type: string + description: Overrides for the `dummy.properties` file. + nullable: true + type: object type: object envOverrides: additionalProperties: diff --git a/crates/stackable-operator/src/commons/resources.rs b/crates/stackable-operator/src/commons/resources.rs index 1eac198a9..ccaf03ec2 100644 --- a/crates/stackable-operator/src/commons/resources.rs +++ b/crates/stackable-operator/src/commons/resources.rs @@ -30,6 +30,9 @@ //! role_utils::Role, //! }; //! +//! #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +//! pub struct ProductConfigOverrides {} +//! //! #[derive(Clone, CustomResource, Debug, Deserialize, JsonSchema, Serialize)] //! #[kube( //! group = "product.stackable.tech", @@ -46,7 +49,7 @@ //! #[serde(rename_all = "camelCase")] //! pub struct ProductSpec { //! #[serde(default, skip_serializing_if = "Option::is_none")] -//! pub nodes: Option>, +//! pub nodes: Option>, //! } //! //! #[derive(Debug, Default, PartialEq, Fragment, JsonSchema)] diff --git a/crates/stackable-operator/src/config_overrides.rs b/crates/stackable-operator/src/config_overrides.rs index 7405abff6..c6a3cfa88 100644 --- a/crates/stackable-operator/src/config_overrides.rs +++ b/crates/stackable-operator/src/config_overrides.rs @@ -5,7 +5,7 @@ //! properties files). The types here are composed by each operator into its //! CRD-specific `configOverrides` struct. -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -34,29 +34,14 @@ pub enum Error { /// Trait that allows the product config pipeline to extract flat key-value /// overrides from any `configOverrides` type. /// -/// The default `HashMap>` implements this -/// by looking up the file name and returning its entries. Typed override -/// structs that have no key-value files can use the default implementation, -/// which returns an empty map. +/// Typed override structs that have no key-value files can use the default +/// implementation, which returns an empty map. pub trait KeyValueOverridesProvider { fn get_key_value_overrides(&self, _file: &str) -> BTreeMap> { BTreeMap::new() } } -impl KeyValueOverridesProvider for HashMap> { - fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { - self.get(file) - .map(|entries| { - entries - .iter() - .map(|(k, v)| (k.clone(), Some(v.clone()))) - .collect() - }) - .unwrap_or_default() - } -} - /// Flat key-value overrides for `*.properties`, Hadoop XML, etc. /// /// This is backwards-compatible with the existing flat key-value YAML format @@ -147,8 +132,6 @@ impl JsonConfigOverrides { #[cfg(test)] mod tests { - use std::collections::HashMap; - use serde_json::json; use super::*; @@ -290,20 +273,4 @@ mod tests { assert_eq!(result.get("key2"), Some(&Some("value2".to_owned()))); } - #[test] - fn key_value_overrides_provider_for_hashmap() { - let mut config_overrides = HashMap::>::new(); - let mut file_overrides = HashMap::new(); - file_overrides.insert("key1".to_owned(), "value1".to_owned()); - file_overrides.insert("key2".to_owned(), "value2".to_owned()); - config_overrides.insert("myfile.properties".to_owned(), file_overrides); - - let result = config_overrides.get_key_value_overrides("myfile.properties"); - assert_eq!(result.len(), 2); - assert_eq!(result.get("key1"), Some(&Some("value1".to_owned()))); - assert_eq!(result.get("key2"), Some(&Some("value2".to_owned()))); - - let empty = config_overrides.get_key_value_overrides("nonexistent.properties"); - assert!(empty.is_empty()); - } } diff --git a/crates/stackable-operator/src/product_config_utils.rs b/crates/stackable-operator/src/product_config_utils.rs index 94614d9d0..ae3f63138 100644 --- a/crates/stackable-operator/src/product_config_utils.rs +++ b/crates/stackable-operator/src/product_config_utils.rs @@ -174,13 +174,13 @@ pub fn config_for_role_and_group<'a>( /// - `roles`: A map keyed by role names. The value is a tuple of a vector of `PropertyNameKind` /// like (Cli, Env or Files) and [`crate::role_utils::Role`] with a boxed [`Configuration`]. #[allow(clippy::type_complexity)] -pub fn transform_all_roles_to_config( +pub fn transform_all_roles_to_config( resource: &T::Configurable, roles: HashMap< String, ( Vec, - Role, + Role, ), >, ) -> Result @@ -384,10 +384,10 @@ fn process_validation_result( /// - `role_name` - The name of the role. /// - `role` - The role for which to transform the configuration parameters. /// - `property_kinds` - Used as "buckets" to partition the configuration properties by. -fn transform_role_to_config( +fn transform_role_to_config( resource: &T::Configurable, role_name: &str, - role: &Role, + role: &Role, property_kinds: &[PropertyNameKind], ) -> Result where @@ -589,7 +589,7 @@ pub fn env_vars_from_rolegroup_config( /// product_config_utils::env_vars_from, role_utils::CommonConfiguration, /// }; /// -/// let common_config = CommonConfiguration::<(), ()> { +/// let common_config = CommonConfiguration::<(), (), ()> { /// env_overrides: [("VAR".to_string(), "value".to_string())] /// .into_iter() /// .collect(), @@ -691,9 +691,30 @@ mod tests { use k8s_openapi::api::core::v1::PodTemplateSpec; use rstest::*; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; use super::*; - use crate::role_utils::{GenericProductSpecificCommonConfig, Role, RoleGroup}; + use crate::{ + config_overrides::{KeyValueConfigOverrides, KeyValueOverridesProvider}, + role_utils::{GenericProductSpecificCommonConfig, Role, RoleGroup}, + }; + + /// Test-only config overrides type that wraps per-file key-value overrides. + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] + struct TestConfigOverrides { + #[serde(flatten)] + files: HashMap, + } + + impl KeyValueOverridesProvider for TestConfigOverrides { + fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { + self.files + .get(file) + .map(|kv| kv.as_overrides()) + .unwrap_or_default() + } + } const ROLE_GROUP: &str = "role_group"; @@ -774,10 +795,11 @@ mod tests { fn build_common_config( test_config: Option>, - config_overrides: Option>>, + config_overrides: Option, env_overrides: Option>, cli_overrides: Option>, - ) -> CommonConfiguration, GenericProductSpecificCommonConfig> { + ) -> CommonConfiguration, GenericProductSpecificCommonConfig, TestConfigOverrides> + { CommonConfiguration { config: test_config.unwrap_or_default(), config_overrides: config_overrides.unwrap_or_default(), @@ -788,13 +810,10 @@ mod tests { } } - fn build_config_override( - file_name: &str, - property: &str, - ) -> Option>> { - Some( - collection!( file_name.to_string() => collection!( property.to_string() => property.to_string())), - ) + fn build_config_override(file_name: &str, property: &str) -> Option { + Some(TestConfigOverrides { + files: collection!( file_name.to_string() => KeyValueConfigOverrides { overrides: collection!( property.to_string() => property.to_string()) }), + }) } fn build_env_override(property: &str) -> Option> { @@ -810,7 +829,7 @@ mod tests { group_config: bool, role_overrides: bool, group_overrides: bool, - ) -> Role, TestRoleConfig> { + ) -> Role, TestConfigOverrides, TestRoleConfig> { let role_group = ROLE_GROUP.to_string(); let file_name = "foo.conf"; @@ -1243,16 +1262,16 @@ mod tests { BTreeMap::from([ ("cli".to_string(), GROUP_CLI_OVERRIDE.to_string()), ]), - HashMap::from([ - ("file".to_string(), HashMap::from([ + TestConfigOverrides { files: HashMap::from([ + ("file".to_string(), KeyValueConfigOverrides { overrides: BTreeMap::from([ ("file".to_string(), ROLE_CONF_OVERRIDE.to_string()) - ])) - ]), - HashMap::from([ - ("file".to_string(), HashMap::from([ + ]) }) + ]) }, + TestConfigOverrides { files: HashMap::from([ + ("file".to_string(), KeyValueConfigOverrides { overrides: BTreeMap::from([ ("file".to_string(), GROUP_CONF_OVERRIDE.to_string()) - ])) - ]), + ]) }) + ]) }, collection ! { ROLE_GROUP.to_string() => collection ! { PropertyNameKind::Env => collection ! { @@ -1276,12 +1295,12 @@ mod tests { ("cli".to_string(), ROLE_CLI_OVERRIDE.to_string()), ]), BTreeMap::from([]), - HashMap::from([ - ("file".to_string(), HashMap::from([ + TestConfigOverrides { files: HashMap::from([ + ("file".to_string(), KeyValueConfigOverrides { overrides: BTreeMap::from([ ("file".to_string(), ROLE_CONF_OVERRIDE.to_string()) - ])) - ]), - HashMap::from([]), + ]) }) + ]) }, + TestConfigOverrides::default(), collection ! { ROLE_GROUP.to_string() => collection ! { PropertyNameKind::Env => collection ! { @@ -1301,8 +1320,8 @@ mod tests { HashMap::from([]), BTreeMap::from([]), BTreeMap::from([]), - HashMap::from([]), - HashMap::from([]), + TestConfigOverrides::default(), + TestConfigOverrides::default(), collection ! { ROLE_GROUP.to_string() => collection ! { PropertyNameKind::Env => collection ! { @@ -1322,14 +1341,14 @@ mod tests { #[case] group_env_override: HashMap, #[case] role_cli_override: BTreeMap, #[case] group_cli_override: BTreeMap, - #[case] role_conf_override: HashMap>, - #[case] group_conf_override: HashMap>, + #[case] role_conf_override: TestConfigOverrides, + #[case] group_conf_override: TestConfigOverrides, #[case] expected: HashMap< String, HashMap>>, >, ) { - let role: Role, TestRoleConfig> = Role { + let role: Role, TestConfigOverrides, TestRoleConfig> = Role { config: build_common_config( build_test_config(ROLE_CONFIG, ROLE_ENV, ROLE_CLI), Some(role_conf_override), @@ -1426,7 +1445,7 @@ mod tests { #[allow(clippy::type_complexity)] let roles: HashMap< String, - (Vec, Role, TestRoleConfig>), + (Vec, Role, TestConfigOverrides, TestRoleConfig>), > = collection! { role_1.to_string() => (vec![PropertyNameKind::File(file_name.to_string()), PropertyNameKind::Env], Role { config: build_common_config( @@ -1523,7 +1542,7 @@ mod tests { #[allow(clippy::type_complexity)] let roles: HashMap< String, - (Vec, Role, TestRoleConfig>), + (Vec, Role, TestConfigOverrides, TestRoleConfig>), > = collection! { role_1.to_string() => (vec![PropertyNameKind::File(file_name.to_string()), PropertyNameKind::Env], Role { config: CommonConfiguration::default(), diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 747af2334..f5fac5e11 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -124,11 +124,7 @@ pub enum Error { #[schemars( bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" )] -pub struct CommonConfiguration< - T, - ProductSpecificCommonConfig, - ConfigOverrides = HashMap>, -> { +pub struct CommonConfiguration { #[serde(default)] // We can't depend on T being `Default`, since that trait is not object-safe // We only need to generate schemas for fully specified types, but schemars_derive @@ -309,9 +305,9 @@ impl JvmArgumentOverrides { #[serde(rename_all = "camelCase")] pub struct Role< T, + ConfigOverrides, U = GenericRoleConfig, ProductSpecificCommonConfig = GenericProductSpecificCommonConfig, - ConfigOverrides = HashMap>, > where // Don't remove this trait bounds!!! // We don't know why, but if you remove either of them, the generated default value in the CRDs will @@ -334,8 +330,8 @@ pub struct Role< pub role_groups: HashMap>, } -impl - Role +impl + Role where T: Configuration + 'static, U: Default + JsonSchema + Serialize, @@ -351,9 +347,9 @@ where self, ) -> Role< Box>, + ConfigOverrides, U, ProductSpecificCommonConfig, - ConfigOverrides, > { Role { config: CommonConfiguration { @@ -393,7 +389,7 @@ where } } -impl Role +impl Role where U: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize, @@ -455,11 +451,7 @@ pub struct EmptyRoleConfig {} #[schemars( bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" )] -pub struct RoleGroup< - T, - ProductSpecificCommonConfig, - ConfigOverrides = HashMap>, -> { +pub struct RoleGroup { #[serde(flatten)] pub config: CommonConfiguration, pub replicas: Option, @@ -470,7 +462,7 @@ impl { pub fn validate_config( &self, - role: &Role, + role: &Role, default_config: &T, ) -> Result where @@ -540,6 +532,9 @@ mod tests { use super::*; use crate::role_utils::JavaCommonConfig; + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] + struct EmptyConfigOverrides {} + #[test] fn test_merge_java_common_config() { // The operator generates some JVM arguments @@ -556,7 +551,7 @@ mod tests { .into(), ); - let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = + let entire_role: Role<(), EmptyConfigOverrides, GenericRoleConfig, JavaCommonConfig> = serde_yaml::from_str(" # Let's say we want to set some additional HTTP Proxy and IPv4 settings # And we don't like the garbage collector for some reason... @@ -618,7 +613,7 @@ mod tests { let operator_generated = JvmArgumentOverrides::new_with_only_additions(["-Xms1m".to_owned()].into()); - let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = serde_yaml::from_str( + let entire_role: Role<(), EmptyConfigOverrides, GenericRoleConfig, JavaCommonConfig> = serde_yaml::from_str( " jvmArgumentOverrides: add: diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index 917e534f3..e4ba8deaf 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -1,7 +1,10 @@ +use std::collections::BTreeMap; + use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::resources::{JvmHeapLimits, Resources}, config::fragment::Fragment, + config_overrides::{JsonConfigOverrides, KeyValueConfigOverrides, KeyValueOverridesProvider}, crd::git_sync::v1alpha2::GitSync, deep_merger::ObjectOverrides, kube::CustomResource, @@ -12,6 +15,40 @@ use stackable_operator::{ }; use strum::EnumIter; +/// Typed config override strategies for Dummy config files. +/// +/// Demonstrates both JSON-formatted (`config.json`) and key-value-formatted +/// (`dummy.properties`) config file overrides. +#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[schemars(crate = "stackable_operator::schemars")] +#[serde(rename_all = "camelCase")] +pub struct DummyConfigOverrides { + /// Overrides for the `config.json` file. + #[serde(default, rename = "config.json", skip_serializing_if = "Option::is_none")] + pub config_json: Option, + + /// Overrides for the `dummy.properties` file. + #[serde( + default, + rename = "dummy.properties", + skip_serializing_if = "Option::is_none" + )] + pub dummy_properties: Option, +} + +impl KeyValueOverridesProvider for DummyConfigOverrides { + fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { + match file { + "dummy.properties" => self + .dummy_properties + .as_ref() + .map(|o| o.as_overrides()) + .unwrap_or_default(), + _ => BTreeMap::new(), + } + } +} + #[versioned( version(name = "v1alpha1"), crates( @@ -33,7 +70,7 @@ pub mod versioned { #[schemars(crate = "stackable_operator::schemars")] #[serde(rename_all = "camelCase")] pub struct DummyClusterSpec { - nodes: Option>, + nodes: Option>, // Not versioned yet stackable_affinity: stackable_operator::commons::affinity::StackableAffinity, @@ -54,9 +91,8 @@ pub mod versioned { #[serde(default)] pub object_overrides: ObjectOverrides, - json_config_overrides: Option, - key_value_config_overrides: - Option, + #[serde(default)] + config_overrides: DummyConfigOverrides, // Already versioned client_authentication_details: From 48ac1440409cb7422c265fdcc95f1c0e3bc310b9 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Mon, 23 Mar 2026 07:50:11 +0000 Subject: [PATCH 6/9] refactor: rename ProductSpecificCommonConfig type parameter to CommonConfig --- .../src/product_config_utils.rs | 30 ++++++------ crates/stackable-operator/src/role_utils.rs | 48 +++++++++---------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/crates/stackable-operator/src/product_config_utils.rs b/crates/stackable-operator/src/product_config_utils.rs index ae3f63138..1d5845e82 100644 --- a/crates/stackable-operator/src/product_config_utils.rs +++ b/crates/stackable-operator/src/product_config_utils.rs @@ -174,20 +174,20 @@ pub fn config_for_role_and_group<'a>( /// - `roles`: A map keyed by role names. The value is a tuple of a vector of `PropertyNameKind` /// like (Cli, Env or Files) and [`crate::role_utils::Role`] with a boxed [`Configuration`]. #[allow(clippy::type_complexity)] -pub fn transform_all_roles_to_config( +pub fn transform_all_roles_to_config( resource: &T::Configurable, roles: HashMap< String, ( Vec, - Role, + Role, ), >, ) -> Result where T: Configuration, U: Default + JsonSchema + Serialize, - ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize + KeyValueOverridesProvider, { let mut result = HashMap::new(); @@ -384,16 +384,16 @@ fn process_validation_result( /// - `role_name` - The name of the role. /// - `role` - The role for which to transform the configuration parameters. /// - `property_kinds` - Used as "buckets" to partition the configuration properties by. -fn transform_role_to_config( +fn transform_role_to_config( resource: &T::Configurable, role_name: &str, - role: &Role, + role: &Role, property_kinds: &[PropertyNameKind], ) -> Result where T: Configuration, U: Default + JsonSchema + Serialize, - ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize + KeyValueOverridesProvider, { let mut result = HashMap::new(); @@ -449,10 +449,10 @@ where /// - `role_name` - Not used directly but passed on to the `Configuration::compute_*` calls. /// - `config` - The configuration properties to partition. /// - `property_kinds` - The "buckets" used to partition the configuration properties. -fn parse_role_config( +fn parse_role_config( resource: &::Configurable, role_name: &str, - config: &CommonConfiguration, + config: &CommonConfiguration, property_kinds: &[PropertyNameKind], ) -> Result>>> where @@ -479,8 +479,8 @@ where Ok(result) } -fn parse_role_overrides( - config: &CommonConfiguration, +fn parse_role_overrides( + config: &CommonConfiguration, property_kinds: &[PropertyNameKind], ) -> Result>>> where @@ -517,8 +517,8 @@ where Ok(result) } -fn parse_file_overrides( - config: &CommonConfiguration, +fn parse_file_overrides( + config: &CommonConfiguration, file: &str, ) -> Result>> where @@ -697,7 +697,7 @@ mod tests { use super::*; use crate::{ config_overrides::{KeyValueConfigOverrides, KeyValueOverridesProvider}, - role_utils::{GenericProductSpecificCommonConfig, Role, RoleGroup}, + role_utils::{GenericCommonConfig, Role, RoleGroup}, }; /// Test-only config overrides type that wraps per-file key-value overrides. @@ -798,7 +798,7 @@ mod tests { config_overrides: Option, env_overrides: Option>, cli_overrides: Option>, - ) -> CommonConfiguration, GenericProductSpecificCommonConfig, TestConfigOverrides> + ) -> CommonConfiguration, GenericCommonConfig, TestConfigOverrides> { CommonConfiguration { config: test_config.unwrap_or_default(), @@ -806,7 +806,7 @@ mod tests { env_overrides: env_overrides.unwrap_or_default(), cli_overrides: cli_overrides.unwrap_or_default(), pod_overrides: PodTemplateSpec::default(), - product_specific_common_config: GenericProductSpecificCommonConfig::default(), + product_specific_common_config: GenericCommonConfig::default(), } } diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index f5fac5e11..e6f9ed560 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -118,13 +118,13 @@ pub enum Error { #[serde( rename_all = "camelCase", bound( - deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" + deserialize = "T: Default + Deserialize<'de>, CommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] #[schemars( - bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" + bound = "T: JsonSchema, CommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" )] -pub struct CommonConfiguration { +pub struct CommonConfiguration { #[serde(default)] // We can't depend on T being `Default`, since that trait is not object-safe // We only need to generate schemas for fully specified types, but schemars_derive @@ -171,11 +171,11 @@ pub struct CommonConfiguration // If [`JavaCommonConfig`] is used, please use [`Role::get_merged_jvm_argument_overrides`] instead of // reading this field directly. #[serde(flatten, default)] - pub product_specific_common_config: ProductSpecificCommonConfig, + pub product_specific_common_config: CommonConfig, } -impl - CommonConfiguration +impl + CommonConfiguration { fn default_config() -> serde_json::Value { serde_json::json!({}) @@ -183,7 +183,7 @@ impl } #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] -pub struct GenericProductSpecificCommonConfig {} +pub struct GenericCommonConfig {} #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -307,35 +307,35 @@ pub struct Role< T, ConfigOverrides, U = GenericRoleConfig, - ProductSpecificCommonConfig = GenericProductSpecificCommonConfig, + CommonConfig = GenericCommonConfig, > where // Don't remove this trait bounds!!! // We don't know why, but if you remove either of them, the generated default value in the CRDs will // be missing! U: Default + JsonSchema + Serialize, - ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize, { #[serde( flatten, bound( - deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Deserialize<'de>, ConfigOverrides: Deserialize<'de>" + deserialize = "T: Default + Deserialize<'de>, CommonConfig: Deserialize<'de>, ConfigOverrides: Deserialize<'de>" ) )] - pub config: CommonConfiguration, + pub config: CommonConfiguration, #[serde(default)] pub role_config: U, - pub role_groups: HashMap>, + pub role_groups: HashMap>, } -impl - Role +impl + Role where T: Configuration + 'static, U: Default + JsonSchema + Serialize, - ProductSpecificCommonConfig: Default + JsonSchema + Serialize + Clone, + CommonConfig: Default + JsonSchema + Serialize + Clone, ConfigOverrides: Default + JsonSchema + Serialize, { /// This casts a generic struct implementing [`crate::product_config_utils::Configuration`] @@ -349,7 +349,7 @@ where Box>, ConfigOverrides, U, - ProductSpecificCommonConfig, + CommonConfig, > { Role { config: CommonConfiguration { @@ -445,31 +445,31 @@ pub struct EmptyRoleConfig {} #[serde( rename_all = "camelCase", bound( - deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" + deserialize = "T: Default + Deserialize<'de>, CommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] #[schemars( - bound = "T: JsonSchema, ProductSpecificCommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" + bound = "T: JsonSchema, CommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" )] -pub struct RoleGroup { +pub struct RoleGroup { #[serde(flatten)] - pub config: CommonConfiguration, + pub config: CommonConfiguration, pub replicas: Option, } -impl - RoleGroup +impl + RoleGroup { pub fn validate_config( &self, - role: &Role, + role: &Role, default_config: &T, ) -> Result where C: FromFragment, T: Merge + Clone, U: Default + JsonSchema + Serialize, - ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize, { let mut role_config = role.config.config.clone(); From 0c5ac02a329de58a8ec1a1ca0c5a896a805d37ee Mon Sep 17 00:00:00 2001 From: dervoeti Date: Mon, 23 Mar 2026 07:59:31 +0000 Subject: [PATCH 7/9] fix: use matches! assertions to verify specific error variants in tests --- crates/stackable-operator/src/config_overrides.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/stackable-operator/src/config_overrides.rs b/crates/stackable-operator/src/config_overrides.rs index c6a3cfa88..6f9a7a994 100644 --- a/crates/stackable-operator/src/config_overrides.rs +++ b/crates/stackable-operator/src/config_overrides.rs @@ -220,7 +220,11 @@ mod tests { ]); let result = overrides.apply(&base); - 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" + ); } #[test] @@ -231,7 +235,8 @@ mod tests { let result = overrides.apply(&base); assert!( - result.is_err(), + 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" ); } @@ -256,7 +261,11 @@ mod tests { let overrides = JsonConfigOverrides::UserProvided("not valid json".to_owned()); let result = overrides.apply(&base); - 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" + ); } #[test] From 4b846013b9594e1ac5c7b8870cebbec1b3d3e724 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Thu, 26 Mar 2026 15:58:23 +0000 Subject: [PATCH 8/9] refactor: rename T and U type parameters to Config and RoleConfig --- .../src/product_config_utils.rs | 40 +++++----- crates/stackable-operator/src/role_utils.rs | 76 +++++++++---------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/crates/stackable-operator/src/product_config_utils.rs b/crates/stackable-operator/src/product_config_utils.rs index 1d5845e82..277d0e523 100644 --- a/crates/stackable-operator/src/product_config_utils.rs +++ b/crates/stackable-operator/src/product_config_utils.rs @@ -174,19 +174,19 @@ pub fn config_for_role_and_group<'a>( /// - `roles`: A map keyed by role names. The value is a tuple of a vector of `PropertyNameKind` /// like (Cli, Env or Files) and [`crate::role_utils::Role`] with a boxed [`Configuration`]. #[allow(clippy::type_complexity)] -pub fn transform_all_roles_to_config( - resource: &T::Configurable, +pub fn transform_all_roles_to_config( + resource: &Config::Configurable, roles: HashMap< String, ( Vec, - Role, + Role, ), >, ) -> Result where - T: Configuration, - U: Default + JsonSchema + Serialize, + Config: Configuration, + RoleConfig: Default + JsonSchema + Serialize, CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize + KeyValueOverridesProvider, { @@ -384,15 +384,15 @@ fn process_validation_result( /// - `role_name` - The name of the role. /// - `role` - The role for which to transform the configuration parameters. /// - `property_kinds` - Used as "buckets" to partition the configuration properties by. -fn transform_role_to_config( - resource: &T::Configurable, +fn transform_role_to_config( + resource: &Config::Configurable, role_name: &str, - role: &Role, + role: &Role, property_kinds: &[PropertyNameKind], ) -> Result where - T: Configuration, - U: Default + JsonSchema + Serialize, + Config: Configuration, + RoleConfig: Default + JsonSchema + Serialize, CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize + KeyValueOverridesProvider, { @@ -449,14 +449,14 @@ where /// - `role_name` - Not used directly but passed on to the `Configuration::compute_*` calls. /// - `config` - The configuration properties to partition. /// - `property_kinds` - The "buckets" used to partition the configuration properties. -fn parse_role_config( - resource: &::Configurable, +fn parse_role_config( + resource: &::Configurable, role_name: &str, - config: &CommonConfiguration, + config: &CommonConfiguration, property_kinds: &[PropertyNameKind], ) -> Result>>> where - T: Configuration, + Config: Configuration, { let mut result = HashMap::new(); for property_kind in property_kinds { @@ -479,12 +479,12 @@ where Ok(result) } -fn parse_role_overrides( - config: &CommonConfiguration, +fn parse_role_overrides( + config: &CommonConfiguration, property_kinds: &[PropertyNameKind], ) -> Result>>> where - T: Configuration, + Config: Configuration, ConfigOverrides: KeyValueOverridesProvider, { let mut result = HashMap::new(); @@ -517,12 +517,12 @@ where Ok(result) } -fn parse_file_overrides( - config: &CommonConfiguration, +fn parse_file_overrides( + config: &CommonConfiguration, file: &str, ) -> Result>> where - T: Configuration, + Config: Configuration, ConfigOverrides: KeyValueOverridesProvider, { Ok(config.config_overrides.get_key_value_overrides(file)) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index e6f9ed560..2b0a621ea 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -118,19 +118,19 @@ pub enum Error { #[serde( rename_all = "camelCase", bound( - deserialize = "T: Default + Deserialize<'de>, CommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" + deserialize = "Config: Default + Deserialize<'de>, CommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] #[schemars( - bound = "T: JsonSchema, CommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" + bound = "Config: JsonSchema, CommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" )] -pub struct CommonConfiguration { +pub struct CommonConfiguration { #[serde(default)] - // We can't depend on T being `Default`, since that trait is not object-safe + // We can't depend on Config being `Default`, since that trait is not object-safe // We only need to generate schemas for fully specified types, but schemars_derive // does not support specifying custom bounds. #[schemars(default = "Self::default_config")] - pub config: T, + pub config: Config, /// The `configOverrides` can be used to configure properties in product config files /// that are not exposed in the CRD. Read the @@ -174,8 +174,8 @@ pub struct CommonConfiguration { pub product_specific_common_config: CommonConfig, } -impl - CommonConfiguration +impl + CommonConfiguration { fn default_config() -> serde_json::Value { serde_json::json!({}) @@ -296,45 +296,45 @@ impl JvmArgumentOverrides { // Everything below is only a "normal" comment, not rustdoc - so we don't bloat the CRD documentation // with technical (Rust) details. // -// `T` here is the `config` shared between role and roleGroup. +// `Config` here is the `config` shared between role and roleGroup. // -// `U` here is the `roleConfig` only available on the role. It defaults to [`GenericRoleConfig`], which is +// `RoleConfig` here is the `roleConfig` only available on the role. It defaults to [`GenericRoleConfig`], which is // sufficient for most of the products. There are some exceptions, where e.g. [`EmptyRoleConfig`] is used. // However, product-operators can define their own - custom - struct and use that here. #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Role< - T, + Config, ConfigOverrides, - U = GenericRoleConfig, + RoleConfig = GenericRoleConfig, CommonConfig = GenericCommonConfig, > where // Don't remove this trait bounds!!! // We don't know why, but if you remove either of them, the generated default value in the CRDs will // be missing! - U: Default + JsonSchema + Serialize, + RoleConfig: Default + JsonSchema + Serialize, CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize, { #[serde( flatten, bound( - deserialize = "T: Default + Deserialize<'de>, CommonConfig: Deserialize<'de>, ConfigOverrides: Deserialize<'de>" + deserialize = "Config: Default + Deserialize<'de>, CommonConfig: Deserialize<'de>, ConfigOverrides: Deserialize<'de>" ) )] - pub config: CommonConfiguration, + pub config: CommonConfiguration, #[serde(default)] - pub role_config: U, + pub role_config: RoleConfig, - pub role_groups: HashMap>, + pub role_groups: HashMap>, } -impl - Role +impl + Role where - T: Configuration + 'static, - U: Default + JsonSchema + Serialize, + Config: Configuration + 'static, + RoleConfig: Default + JsonSchema + Serialize, CommonConfig: Default + JsonSchema + Serialize + Clone, ConfigOverrides: Default + JsonSchema + Serialize, { @@ -346,15 +346,15 @@ where pub fn erase( self, ) -> Role< - Box>, + Box>, ConfigOverrides, - U, + RoleConfig, CommonConfig, > { Role { config: CommonConfiguration { config: Box::new(self.config.config) - as Box>, + as Box>, config_overrides: self.config.config_overrides, env_overrides: self.config.env_overrides, cli_overrides: self.config.cli_overrides, @@ -371,7 +371,7 @@ where RoleGroup { config: CommonConfiguration { config: Box::new(group.config.config) - as Box>, + as Box>, config_overrides: group.config.config_overrides, env_overrides: group.config.env_overrides, cli_overrides: group.config.cli_overrides, @@ -389,9 +389,9 @@ where } } -impl Role +impl Role where - U: Default + JsonSchema + Serialize, + RoleConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize, { /// Merges jvm argument overrides from @@ -445,30 +445,30 @@ pub struct EmptyRoleConfig {} #[serde( rename_all = "camelCase", bound( - deserialize = "T: Default + Deserialize<'de>, CommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" + deserialize = "Config: Default + Deserialize<'de>, CommonConfig: Default + Deserialize<'de>, ConfigOverrides: Default + Deserialize<'de>" ) )] #[schemars( - bound = "T: JsonSchema, CommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" + bound = "Config: JsonSchema, CommonConfig: JsonSchema, ConfigOverrides: Default + JsonSchema" )] -pub struct RoleGroup { +pub struct RoleGroup { #[serde(flatten)] - pub config: CommonConfiguration, + pub config: CommonConfiguration, pub replicas: Option, } -impl - RoleGroup +impl + RoleGroup { - pub fn validate_config( + pub fn validate_config( &self, - role: &Role, - default_config: &T, + role: &Role, + default_config: &Config, ) -> Result where - C: FromFragment, - T: Merge + Clone, - U: Default + JsonSchema + Serialize, + C: FromFragment, + Config: Merge + Clone, + RoleConfig: Default + JsonSchema + Serialize, CommonConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize, { From b19c190c748b7ceefb7735b514ea6e2fe63a35a8 Mon Sep 17 00:00:00 2001 From: dervoeti Date: Thu, 26 Mar 2026 16:55:48 +0000 Subject: [PATCH 9/9] fix: apply rustfmt formatting --- .../stackable-operator/src/config_overrides.rs | 1 - .../src/product_config_utils.rs | 13 +++++++++---- crates/stackable-operator/src/role_utils.rs | 16 ++++++++-------- crates/xtask/src/crd/dummy.rs | 6 +++++- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/stackable-operator/src/config_overrides.rs b/crates/stackable-operator/src/config_overrides.rs index 6f9a7a994..0cbc63eeb 100644 --- a/crates/stackable-operator/src/config_overrides.rs +++ b/crates/stackable-operator/src/config_overrides.rs @@ -281,5 +281,4 @@ mod tests { assert_eq!(result.get("key1"), Some(&Some("value1".to_owned()))); assert_eq!(result.get("key2"), Some(&Some("value2".to_owned()))); } - } diff --git a/crates/stackable-operator/src/product_config_utils.rs b/crates/stackable-operator/src/product_config_utils.rs index 277d0e523..67c43fdeb 100644 --- a/crates/stackable-operator/src/product_config_utils.rs +++ b/crates/stackable-operator/src/product_config_utils.rs @@ -798,8 +798,7 @@ mod tests { config_overrides: Option, env_overrides: Option>, cli_overrides: Option>, - ) -> CommonConfiguration, GenericCommonConfig, TestConfigOverrides> - { + ) -> CommonConfiguration, GenericCommonConfig, TestConfigOverrides> { CommonConfiguration { config: test_config.unwrap_or_default(), config_overrides: config_overrides.unwrap_or_default(), @@ -1445,7 +1444,10 @@ mod tests { #[allow(clippy::type_complexity)] let roles: HashMap< String, - (Vec, Role, TestConfigOverrides, TestRoleConfig>), + ( + Vec, + Role, TestConfigOverrides, TestRoleConfig>, + ), > = collection! { role_1.to_string() => (vec![PropertyNameKind::File(file_name.to_string()), PropertyNameKind::Env], Role { config: build_common_config( @@ -1542,7 +1544,10 @@ mod tests { #[allow(clippy::type_complexity)] let roles: HashMap< String, - (Vec, Role, TestConfigOverrides, TestRoleConfig>), + ( + Vec, + Role, TestConfigOverrides, TestRoleConfig>, + ), > = collection! { role_1.to_string() => (vec![PropertyNameKind::File(file_name.to_string()), PropertyNameKind::Env], Role { config: CommonConfiguration::default(), diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 2b0a621ea..4b0984b24 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -389,7 +389,8 @@ where } } -impl Role +impl + Role where RoleConfig: Default + JsonSchema + Serialize, ConfigOverrides: Default + JsonSchema + Serialize, @@ -457,9 +458,7 @@ pub struct RoleGroup { pub replicas: Option, } -impl - RoleGroup -{ +impl RoleGroup { pub fn validate_config( &self, role: &Role, @@ -613,8 +612,9 @@ mod tests { let operator_generated = JvmArgumentOverrides::new_with_only_additions(["-Xms1m".to_owned()].into()); - let entire_role: Role<(), EmptyConfigOverrides, GenericRoleConfig, JavaCommonConfig> = serde_yaml::from_str( - " + let entire_role: Role<(), EmptyConfigOverrides, GenericRoleConfig, JavaCommonConfig> = + serde_yaml::from_str( + " jvmArgumentOverrides: add: - -Xms2m @@ -624,8 +624,8 @@ mod tests { add: - -Xms3m ", - ) - .expect("Failed to parse role"); + ) + .expect("Failed to parse role"); let merged_jvm_argument_overrides = entire_role .get_merged_jvm_argument_overrides("default", &operator_generated) diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index e4ba8deaf..7edef1043 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -24,7 +24,11 @@ use strum::EnumIter; #[serde(rename_all = "camelCase")] pub struct DummyConfigOverrides { /// Overrides for the `config.json` file. - #[serde(default, rename = "config.json", skip_serializing_if = "Option::is_none")] + #[serde( + default, + rename = "config.json", + skip_serializing_if = "Option::is_none" + )] pub config_json: Option, /// Overrides for the `dummy.properties` file.