From ce932f0110ec7f4d0bf72a943a0cade52b8a471e Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 6 Dec 2024 11:48:59 +0100 Subject: [PATCH 01/23] WIP: First draft of ProductSpecificCommonConfig --- Cargo.lock | 4 +- .../src/product_config_utils.rs | 34 ++++-- crates/stackable-operator/src/role_utils.rs | 111 +++++++++++++++--- 3 files changed, 118 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cdc26c21..222e62c07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3277,14 +3277,14 @@ dependencies = [ [[package]] name = "stackable-versioned" -version = "0.4.1" +version = "0.5.0" dependencies = [ "stackable-versioned-macros", ] [[package]] name = "stackable-versioned-macros" -version = "0.4.1" +version = "0.5.0" dependencies = [ "convert_case", "darling", diff --git a/crates/stackable-operator/src/product_config_utils.rs b/crates/stackable-operator/src/product_config_utils.rs index 1d1a68c6a..240957751 100644 --- a/crates/stackable-operator/src/product_config_utils.rs +++ b/crates/stackable-operator/src/product_config_utils.rs @@ -167,13 +167,21 @@ pub fn config_for_role_and_group<'a>( /// - `resource` - Not used directly. It's passed on to the `Configuration::compute_*` calls. /// - `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`]. -pub fn transform_all_roles_to_config( +#[allow(clippy::type_complexity)] +pub fn transform_all_roles_to_config( resource: &T::Configurable, - roles: HashMap, Role)>, + roles: HashMap< + String, + ( + Vec, + Role, + ), + >, ) -> Result where T: Configuration, U: Default + JsonSchema + Serialize, + ProductSpecificCommonConfig: Default + JsonSchema + Serialize, { let mut result = HashMap::new(); @@ -359,15 +367,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, { let mut result = HashMap::new(); @@ -422,10 +431,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 @@ -452,8 +461,8 @@ where Ok(result) } -fn parse_role_overrides( - config: &CommonConfiguration, +fn parse_role_overrides( + config: &CommonConfiguration, property_kinds: &[PropertyNameKind], ) -> Result>>> where @@ -489,8 +498,8 @@ where Ok(result) } -fn parse_file_overrides( - config: &CommonConfiguration, +fn parse_file_overrides( + config: &CommonConfiguration, file: &str, ) -> Result>> where @@ -522,7 +531,7 @@ mod tests { } use super::*; - use crate::role_utils::{Role, RoleGroup}; + use crate::role_utils::{GenericProductSpecificCommonConfig, Role, RoleGroup}; use k8s_openapi::api::core::v1::PodTemplateSpec; use rstest::*; use std::collections::HashMap; @@ -610,13 +619,14 @@ mod tests { config_overrides: Option>>, env_overrides: Option>, cli_overrides: Option>, - ) -> CommonConfiguration> { + ) -> CommonConfiguration, GenericProductSpecificCommonConfig> { CommonConfiguration { config: test_config.unwrap_or_default(), config_overrides: config_overrides.unwrap_or_default(), env_overrides: env_overrides.unwrap_or_default(), cli_overrides: cli_overrides.unwrap_or_default(), pod_overrides: PodTemplateSpec::default(), + product_specific_common_config: GenericProductSpecificCommonConfig::default(), } } diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 04ecd2593..370a9b82c 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -89,7 +89,7 @@ use crate::{ commons::pdb::PdbConfig, config::{ fragment::{self, FromFragment}, - merge::Merge, + merge::{Atomic, Merge}, }, product_config_utils::Configuration, utils::crds::raw_object_schema, @@ -99,18 +99,27 @@ use k8s_openapi::api::core::v1::PodTemplateSpec; use kube::{runtime::reflector::ObjectRef, Resource}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use snafu::{OptionExt, Snafu}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("missing roleGroup {role_group:?}"))] + MissingRoleGroup { role_group: String }, +} #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde( rename_all = "camelCase", - bound(deserialize = "T: Default + Deserialize<'de>") + bound( + deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>" + ) )] -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 // does not support specifying custom bounds. - #[schemars(default = "config_schema_default")] + #[schemars(default = "Self::default_config")] pub config: T, /// The `configOverrides` can be used to configure properties in product config files @@ -144,10 +153,42 @@ pub struct CommonConfiguration { #[serde(default)] #[schemars(schema_with = "raw_object_schema")] pub pod_overrides: PodTemplateSpec, + + // No docs needed, as we flatten this struct. + // + // This field is product-specific and can contain e.g. jvmArgumentOverrides. + // It is not accessible by operators, please use to read the value + #[serde(flatten, default)] + pub(crate) product_specific_common_config: ProductSpecificCommonConfig, +} + +impl CommonConfiguration { + fn default_config() -> serde_json::Value { + serde_json::json!({}) + } +} + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct GenericProductSpecificCommonConfig {} + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize, Merge)] +#[merge(path_overrides(merge = "crate::config::merge"))] +#[serde(rename_all = "camelCase")] +pub struct JavaCommonConfig { + /// Allows overriding JVM arguments. + /// + // TODO: Docs + #[serde(default)] + pub jvm_argument_overrides: BTreeMap, } -fn config_schema_default() -> serde_json::Value { - serde_json::json!({}) +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct JvmArgument(pub Option); +impl Atomic for JvmArgument {} +impl Merge for JvmArgument { + fn merge(&mut self, defaults: &Self) { + *self = defaults.clone(); + } } /// This struct represents a role - e.g. HDFS datanodes or Trino workers. It has a key-value-map containing @@ -168,33 +209,46 @@ fn config_schema_default() -> serde_json::Value { // 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 -where +pub struct Role< + T, + U = GenericRoleConfig, + ProductSpecificCommonConfig = GenericProductSpecificCommonConfig, +> 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, { - #[serde(flatten, bound(deserialize = "T: Default + Deserialize<'de>"))] - pub config: CommonConfiguration, + #[serde( + flatten, + bound( + deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Deserialize<'de>" + ) + )] + 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 + Merge, { /// This casts a generic struct implementing [`crate::product_config_utils::Configuration`] /// and used in [`Role`] into a Box of a dynamically dispatched /// [`crate::product_config_utils::Configuration`] Trait. This is required to use the generic /// [`Role`] with more than a single generic struct. For example different roles most likely /// have different structs implementing Configuration. - pub fn erase(self) -> Role>, U> { + pub fn erase( + self, + ) -> Role>, U, ProductSpecificCommonConfig> + { Role { config: CommonConfiguration { config: Box::new(self.config.config) @@ -203,6 +257,7 @@ where env_overrides: self.config.env_overrides, cli_overrides: self.config.cli_overrides, pod_overrides: self.config.pod_overrides, + product_specific_common_config: self.config.product_specific_common_config, }, role_config: self.role_config, role_groups: self @@ -219,6 +274,9 @@ where env_overrides: group.config.env_overrides, cli_overrides: group.config.cli_overrides, pod_overrides: group.config.pod_overrides, + product_specific_common_config: group + .config + .product_specific_common_config, }, replicas: group.replicas, }, @@ -227,6 +285,23 @@ where .collect(), } } + + pub fn merged_product_specific_common_config( + &self, + role_group: &str, + ) -> Result { + let from_role = &self.config.product_specific_common_config; + let mut merged = self + .role_groups + .get(role_group) + .with_context(|| MissingRoleGroupSnafu { role_group })? + .config + .product_specific_common_config + .clone(); + merged.merge(from_role); + + Ok(merged) + } } /// This is a product-agnostic RoleConfig, which is sufficient for most of the products. @@ -246,15 +321,17 @@ pub struct EmptyRoleConfig {} #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde( rename_all = "camelCase", - bound(deserialize = "T: Default + Deserialize<'de>") + bound( + deserialize = "T: Default + Deserialize<'de>, ProductSpecificCommonConfig: Default + Deserialize<'de>" + ) )] -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, From 3462c9e9b670d17b07150e4a4a617bb944bb1b5a Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 6 Dec 2024 14:01:46 +0100 Subject: [PATCH 02/23] v2: Allow deletion of operator generated arguments --- crates/stackable-operator/src/role_utils.rs | 223 +++++++++++++++++++- 1 file changed, 217 insertions(+), 6 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 370a9b82c..ffbce666f 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -89,7 +89,7 @@ use crate::{ commons::pdb::PdbConfig, config::{ fragment::{self, FromFragment}, - merge::{Atomic, Merge}, + merge::Merge, }, product_config_utils::Configuration, utils::crds::raw_object_schema, @@ -178,16 +178,38 @@ pub struct JavaCommonConfig { /// Allows overriding JVM arguments. /// // TODO: Docs + // Use [`JavaCommonConfig::effective_jvm_config`] to retrieve the effective JVM arguments! #[serde(default)] pub jvm_argument_overrides: BTreeMap, } -#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] -pub struct JvmArgument(pub Option); -impl Atomic for JvmArgument {} +impl JavaCommonConfig { + /// Returns all arguments that should be passed to the JVM. + /// + /// Please note that the values of the [`BTreeMap`] are [`Option`]. A value of [`None`] + /// expresses that the given argument is just a flag without any argument. + pub fn effective_jvm_config(&self) -> BTreeMap> { + self.jvm_argument_overrides + .iter() + .filter_map(|(k, v)| match v { + JvmArgument::Argument(argument) => Some((k.to_owned(), Some(argument.to_owned()))), + JvmArgument::Flag {} => Some((k.to_owned(), None)), + JvmArgument::Remove {} => None, + }) + .collect() + } +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum JvmArgument { + Argument(String), + Flag {}, + Remove {}, +} impl Merge for JvmArgument { - fn merge(&mut self, defaults: &Self) { - *self = defaults.clone(); + fn merge(&mut self, _defaults: &Self) { + // We ignore whatever was in there before, later values override earlier ones } } @@ -373,3 +395,192 @@ impl Display for RoleGroupRef { )) } } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use crate::{config::merge::Merge, role_utils::JvmArgument}; + + use super::JavaCommonConfig; + + #[test] + fn test_parse_java_common_config() { + let input = r#" + jvmArgumentOverrides: + -XX:+UseG1GC: + flag: {} + -Dhttps.proxyHost: + argument: proxy.my.corp + -XX:+ExitOnOutOfMemoryError: + remove: {} + "#; + let deserializer = serde_yaml::Deserializer::from_str(input); + let java_common_config: JavaCommonConfig = + serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); + + assert_eq!( + java_common_config, + JavaCommonConfig { + jvm_argument_overrides: BTreeMap::from([ + ("-XX:+UseG1GC".to_owned(), JvmArgument::Flag {}), + ( + "-Dhttps.proxyHost".to_owned(), + JvmArgument::Argument("proxy.my.corp".to_owned()) + ), + ( + "-XX:+ExitOnOutOfMemoryError".to_owned(), + JvmArgument::Remove {} + ) + ]) + } + ); + } + + #[test] + fn test_merge_java_common_config() { + // The operator generates some JVM arguments + let operator_generated = JavaCommonConfig { + jvm_argument_overrides: BTreeMap::from([ + // Some flags + ("-Xms34406m".to_owned(), JvmArgument::Flag {}), + ("-Xmx34406m".to_owned(), JvmArgument::Flag {}), + ("-XX:+UseG1GC".to_owned(), JvmArgument::Flag {}), + ( + "-XX:+ExitOnOutOfMemoryError".to_owned(), + JvmArgument::Flag {}, + ), + // And some arguments + ( + "-Djava.protocol.handler.pkgs".to_owned(), + JvmArgument::Argument("sun.net.www.protocol".to_owned()), + ), + ( + "-Dsun.net.http.allowRestrictedHeaders".to_owned(), + JvmArgument::Argument(true.to_string()), + ), + ( + "-Djava.security.properties".to_owned(), + JvmArgument::Argument("/stackable/nifi/conf/security.properties".to_owned()), + ), + ]), + }; + + // 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... + let role = serde_yaml::Deserializer::from_str( + r#" + jvmArgumentOverrides: + -XX:+UseG1GC: + remove: {} + -Dhttps.proxyHost: + argument: proxy.my.corp + -Dhttps.proxyPort: + argument: "8080" + -Djava.net.preferIPv4Stack: + argument: "true" + "#, + ); + let role: JavaCommonConfig = + serde_yaml::with::singleton_map_recursive::deserialize(role).unwrap(); + + // For the roleGroup, let's say we need a different memory config. + // For that to work we first remove the flags generated by the operator and add our own. + // Also we override the proxy port to test that the roleGroup config takes precedence over the role config. + let role_group = serde_yaml::Deserializer::from_str( + r#" + jvmArgumentOverrides: + # We need more memory! + -Xmx34406m: + remove: {} + -Xmx40000m: + flag: {} + -Dhttps.proxyPort: + argument: "1234" + "#, + ); + let role_group: JavaCommonConfig = + serde_yaml::with::singleton_map_recursive::deserialize(role_group).unwrap(); + + let mut merged = role_group; + merged.merge(&role); + merged.merge(&operator_generated); + + assert_eq!( + merged, + JavaCommonConfig { + jvm_argument_overrides: BTreeMap::from([ + // Flags + ("-Xms34406m".to_owned(), JvmArgument::Flag {}), + // Note the different memory config from the roleGroup! + ("-Xmx34406m".to_owned(), JvmArgument::Remove {}), + ("-Xmx40000m".to_owned(), JvmArgument::Flag {}), + // Note that the "-XX:+UseG1GC" flag is removed! + ("-XX:+UseG1GC".to_owned(), JvmArgument::Remove {}), + ( + "-XX:+ExitOnOutOfMemoryError".to_owned(), + JvmArgument::Flag {}, + ), + // Arguments + ( + "-Djava.protocol.handler.pkgs".to_owned(), + JvmArgument::Argument("sun.net.www.protocol".to_owned()), + ), + ( + "-Dsun.net.http.allowRestrictedHeaders".to_owned(), + JvmArgument::Argument(true.to_string()), + ), + ( + "-Djava.security.properties".to_owned(), + JvmArgument::Argument( + "/stackable/nifi/conf/security.properties".to_owned() + ), + ), + ( + "-Dhttps.proxyHost".to_owned(), + JvmArgument::Argument("proxy.my.corp".to_owned()), + ), + ( + "-Dhttps.proxyPort".to_owned(), + // Note: This is overridden by the roleGroup + JvmArgument::Argument("1234".to_owned()), + ), + ( + "-Djava.net.preferIPv4Stack".to_owned(), + JvmArgument::Argument("true".to_owned()), + ), + ]) + } + ); + + assert_eq!( + merged.effective_jvm_config(), + BTreeMap::from([ + ("-Xms34406m".to_owned(), None), + ("-Xmx40000m".to_owned(), None), + ("-XX:+ExitOnOutOfMemoryError".to_owned(), None), + ( + "-Djava.protocol.handler.pkgs".to_owned(), + Some("sun.net.www.protocol".to_owned()) + ), + ( + "-Dsun.net.http.allowRestrictedHeaders".to_owned(), + Some("true".to_owned()) + ), + ( + "-Djava.security.properties".to_owned(), + Some("/stackable/nifi/conf/security.properties".to_owned()) + ), + ( + "-Dhttps.proxyHost".to_owned(), + Some("proxy.my.corp".to_owned()) + ), + ("-Dhttps.proxyPort".to_owned(), Some("1234".to_owned())), + ( + "-Djava.net.preferIPv4Stack".to_owned(), + Some("true".to_owned()) + ), + ]) + ); + } +} From a7ae846f88c2cc0e4f0f41f2664f6c807f702b4c Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Mon, 9 Dec 2024 13:17:27 +0100 Subject: [PATCH 03/23] refactor!: Make field private --- crates/stackable-operator/src/role_utils.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index ffbce666f..1ac5d5637 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -180,10 +180,16 @@ pub struct JavaCommonConfig { // TODO: Docs // Use [`JavaCommonConfig::effective_jvm_config`] to retrieve the effective JVM arguments! #[serde(default)] - pub jvm_argument_overrides: BTreeMap, + jvm_argument_overrides: BTreeMap, } impl JavaCommonConfig { + pub fn new(jvm_argument_overrides: BTreeMap) -> Self { + Self { + jvm_argument_overrides, + } + } + /// Returns all arguments that should be passed to the JVM. /// /// Please note that the values of the [`BTreeMap`] are [`Option`]. A value of [`None`] From 5ee9d11ba2926012055c38dee3c9233898987f46 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 20 Dec 2024 10:43:05 +0100 Subject: [PATCH 04/23] WIP: Try out add, remove and removeRegex --- crates/stackable-operator/src/role_utils.rs | 338 +++++++++----------- 1 file changed, 152 insertions(+), 186 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 1ac5d5637..a50670e99 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -81,7 +81,7 @@ //! Each resource can have more operator specific labels. use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt::{Debug, Display}, }; @@ -97,9 +97,11 @@ use crate::{ use educe::Educe; use k8s_openapi::api::core::v1::PodTemplateSpec; use kube::{runtime::reflector::ObjectRef, Resource}; +use regex::Regex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, Snafu}; +use tracing::instrument; #[derive(Debug, Snafu)] pub enum Error { @@ -176,46 +178,98 @@ pub struct GenericProductSpecificCommonConfig {} #[serde(rename_all = "camelCase")] pub struct JavaCommonConfig { /// Allows overriding JVM arguments. - /// + // // TODO: Docs - // Use [`JavaCommonConfig::effective_jvm_config`] to retrieve the effective JVM arguments! #[serde(default)] - jvm_argument_overrides: BTreeMap, + pub jvm_argument_overrides: JvmArgumentOverrides, +} + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JvmArgumentOverrides { + #[serde(default)] + add: BTreeSet, + + #[serde(default)] + remove: BTreeSet, + + #[serde(default)] + remove_regex: BTreeSet, } -impl JavaCommonConfig { - pub fn new(jvm_argument_overrides: BTreeMap) -> Self { +impl JvmArgumentOverrides { + pub fn new( + add: BTreeSet, + remove: BTreeSet, + remove_regex: BTreeSet, + ) -> Self { + Self { + add, + remove, + remove_regex, + } + } + + pub fn new_with_only_additions(add: BTreeSet) -> Self { Self { - jvm_argument_overrides, + add, + ..Default::default() } } /// Returns all arguments that should be passed to the JVM. /// - /// Please note that the values of the [`BTreeMap`] are [`Option`]. A value of [`None`] - /// expresses that the given argument is just a flag without any argument. - pub fn effective_jvm_config(&self) -> BTreeMap> { - self.jvm_argument_overrides - .iter() - .filter_map(|(k, v)| match v { - JvmArgument::Argument(argument) => Some((k.to_owned(), Some(argument.to_owned()))), - JvmArgument::Flag {} => Some((k.to_owned(), None)), - JvmArgument::Remove {} => None, - }) - .collect() + /// **Can only be called on merged config, it will panic otherwise** + pub fn effective_jvm_config_after_merging(&self) -> &BTreeSet { + assert!( + self.remove.is_empty(), + "After merging there should be no removals left. \"effective_jvm_config_after_merging\" should only be called on merged configs!" + ); + assert!( + self.remove_regex.is_empty(), + "After merging there should be no removal regexes left. \"effective_jvm_config_after_merging\" should only be called on merged configs!" + ); + + &self.add } } -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum JvmArgument { - Argument(String), - Flag {}, - Remove {}, -} -impl Merge for JvmArgument { - fn merge(&mut self, _defaults: &Self) { - // We ignore whatever was in there before, later values override earlier ones +impl Merge for JvmArgumentOverrides { + #[instrument] + fn merge(&mut self, defaults: &Self) { + let regexes = self + .remove_regex + .iter() + .filter_map(|regex| { + let without_anchors = regex.trim_start_matches('^').trim_end_matches('$'); + let with_anchors = format!("^{without_anchors}$"); + match Regex::new(&with_anchors) { + Ok(regex) => Some(regex), + Err(error) => { + tracing::warn!( + regex, + regex_without_anchors = without_anchors, + regex_with_anchors = with_anchors, + ?error, + "Could not parse regex from \"jvmArgumentOverrides.removeRegex\", ignoring it" + ); + None + } + }}) + .collect::>(); + + let new_add = defaults + .add + .iter() + .filter(|arg| !self.remove.contains(*arg)) + .filter(|arg| !regexes.iter().any(|regex| regex.is_match(arg))) + .chain(self.add.iter()) + .cloned() + .collect(); + + self.add = new_add; + self.remove = BTreeSet::new(); + self.remove_regex = BTreeSet::new(); } } @@ -404,189 +458,101 @@ impl Display for RoleGroupRef { #[cfg(test)] mod tests { - use std::collections::BTreeMap; + use std::collections::BTreeSet; - use crate::{config::merge::Merge, role_utils::JvmArgument}; + use crate::{config::merge::Merge, role_utils::JavaCommonConfig}; - use super::JavaCommonConfig; - - #[test] - fn test_parse_java_common_config() { - let input = r#" - jvmArgumentOverrides: - -XX:+UseG1GC: - flag: {} - -Dhttps.proxyHost: - argument: proxy.my.corp - -XX:+ExitOnOutOfMemoryError: - remove: {} - "#; - let deserializer = serde_yaml::Deserializer::from_str(input); - let java_common_config: JavaCommonConfig = - serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); - - assert_eq!( - java_common_config, - JavaCommonConfig { - jvm_argument_overrides: BTreeMap::from([ - ("-XX:+UseG1GC".to_owned(), JvmArgument::Flag {}), - ( - "-Dhttps.proxyHost".to_owned(), - JvmArgument::Argument("proxy.my.corp".to_owned()) - ), - ( - "-XX:+ExitOnOutOfMemoryError".to_owned(), - JvmArgument::Remove {} - ) - ]) - } - ); - } + use super::JvmArgumentOverrides; #[test] fn test_merge_java_common_config() { // The operator generates some JVM arguments let operator_generated = JavaCommonConfig { - jvm_argument_overrides: BTreeMap::from([ - // Some flags - ("-Xms34406m".to_owned(), JvmArgument::Flag {}), - ("-Xmx34406m".to_owned(), JvmArgument::Flag {}), - ("-XX:+UseG1GC".to_owned(), JvmArgument::Flag {}), - ( + jvm_argument_overrides: JvmArgumentOverrides::new_with_only_additions( + [ + "-Xms34406m".to_owned(), + "-Xmx34406m".to_owned(), + "-XX:+UseG1GC".to_owned(), "-XX:+ExitOnOutOfMemoryError".to_owned(), - JvmArgument::Flag {}, - ), - // And some arguments - ( - "-Djava.protocol.handler.pkgs".to_owned(), - JvmArgument::Argument("sun.net.www.protocol".to_owned()), - ), - ( - "-Dsun.net.http.allowRestrictedHeaders".to_owned(), - JvmArgument::Argument(true.to_string()), - ), - ( - "-Djava.security.properties".to_owned(), - JvmArgument::Argument("/stackable/nifi/conf/security.properties".to_owned()), - ), - ]), + "-Djava.protocol.handler.pkgs=sun.net.www.protocol".to_owned(), + "-Dsun.net.http.allowRestrictedHeaders=true".to_owned(), + "-Djava.security.properties=/stackable/nifi/conf/security.properties" + .to_owned(), + ] + .into(), + ), }; // 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... - let role = serde_yaml::Deserializer::from_str( + let mut role: JavaCommonConfig = serde_yaml::from_str( r#" - jvmArgumentOverrides: - -XX:+UseG1GC: - remove: {} - -Dhttps.proxyHost: - argument: proxy.my.corp - -Dhttps.proxyPort: - argument: "8080" - -Djava.net.preferIPv4Stack: - argument: "true" - "#, - ); - let role: JavaCommonConfig = - serde_yaml::with::singleton_map_recursive::deserialize(role).unwrap(); + jvmArgumentOverrides: + remove: + - -XX:+UseG1GC + add: + - -Dhttps.proxyHost=proxy.my.corp + - -Dhttps.proxyPort=8080 + - -Djava.net.preferIPv4Stack=true + "#, + ) + .unwrap(); // For the roleGroup, let's say we need a different memory config. // For that to work we first remove the flags generated by the operator and add our own. // Also we override the proxy port to test that the roleGroup config takes precedence over the role config. - let role_group = serde_yaml::Deserializer::from_str( + let mut role_group: JavaCommonConfig = serde_yaml::from_str( r#" - jvmArgumentOverrides: - # We need more memory! - -Xmx34406m: - remove: {} - -Xmx40000m: - flag: {} - -Dhttps.proxyPort: - argument: "1234" - "#, - ); - let role_group: JavaCommonConfig = - serde_yaml::with::singleton_map_recursive::deserialize(role_group).unwrap(); - - let mut merged = role_group; - merged.merge(&role); - merged.merge(&operator_generated); + jvmArgumentOverrides: + removeRegex: + - # We need more memory! + - -Xmx.* + - -Dhttps.proxyPort=.* + add: + - -Xmx40000m + - -Dhttps.proxyPort=1234 + "#, + ) + .unwrap(); + + // let mut merged = role_group; + // merged.merge(&role); + // merged.merge(&operator_generated); + + // Please note that merge order is different than we normally do! + // This is not trivial, as the merge operation is not purely additive (as it is with e.g. + // PodTemplateSpec). + role.merge(&operator_generated); + role_group.merge(&role); + + let expected = BTreeSet::from([ + "-Xms34406m".to_owned(), + "-Xmx40000m".to_owned(), + "-XX:+ExitOnOutOfMemoryError".to_owned(), + "-Djava.protocol.handler.pkgs=sun.net.www.protocol".to_owned(), + "-Dsun.net.http.allowRestrictedHeaders=true".to_owned(), + "-Djava.security.properties=/stackable/nifi/conf/security.properties".to_owned(), + "-Djava.net.preferIPv4Stack=true".to_owned(), + "-Dhttps.proxyHost=proxy.my.corp".to_owned(), + "-Dhttps.proxyPort=1234".to_owned(), + ]); assert_eq!( - merged, + role_group, JavaCommonConfig { - jvm_argument_overrides: BTreeMap::from([ - // Flags - ("-Xms34406m".to_owned(), JvmArgument::Flag {}), - // Note the different memory config from the roleGroup! - ("-Xmx34406m".to_owned(), JvmArgument::Remove {}), - ("-Xmx40000m".to_owned(), JvmArgument::Flag {}), - // Note that the "-XX:+UseG1GC" flag is removed! - ("-XX:+UseG1GC".to_owned(), JvmArgument::Remove {}), - ( - "-XX:+ExitOnOutOfMemoryError".to_owned(), - JvmArgument::Flag {}, - ), - // Arguments - ( - "-Djava.protocol.handler.pkgs".to_owned(), - JvmArgument::Argument("sun.net.www.protocol".to_owned()), - ), - ( - "-Dsun.net.http.allowRestrictedHeaders".to_owned(), - JvmArgument::Argument(true.to_string()), - ), - ( - "-Djava.security.properties".to_owned(), - JvmArgument::Argument( - "/stackable/nifi/conf/security.properties".to_owned() - ), - ), - ( - "-Dhttps.proxyHost".to_owned(), - JvmArgument::Argument("proxy.my.corp".to_owned()), - ), - ( - "-Dhttps.proxyPort".to_owned(), - // Note: This is overridden by the roleGroup - JvmArgument::Argument("1234".to_owned()), - ), - ( - "-Djava.net.preferIPv4Stack".to_owned(), - JvmArgument::Argument("true".to_owned()), - ), - ]) + jvm_argument_overrides: JvmArgumentOverrides { + add: expected.clone(), + remove: BTreeSet::new(), + remove_regex: BTreeSet::new() + } } ); assert_eq!( - merged.effective_jvm_config(), - BTreeMap::from([ - ("-Xms34406m".to_owned(), None), - ("-Xmx40000m".to_owned(), None), - ("-XX:+ExitOnOutOfMemoryError".to_owned(), None), - ( - "-Djava.protocol.handler.pkgs".to_owned(), - Some("sun.net.www.protocol".to_owned()) - ), - ( - "-Dsun.net.http.allowRestrictedHeaders".to_owned(), - Some("true".to_owned()) - ), - ( - "-Djava.security.properties".to_owned(), - Some("/stackable/nifi/conf/security.properties".to_owned()) - ), - ( - "-Dhttps.proxyHost".to_owned(), - Some("proxy.my.corp".to_owned()) - ), - ("-Dhttps.proxyPort".to_owned(), Some("1234".to_owned())), - ( - "-Djava.net.preferIPv4Stack".to_owned(), - Some("true".to_owned()) - ), - ]) + role_group + .jvm_argument_overrides + .effective_jvm_config_after_merging(), + &expected ); } } From cc1bad832e158c1ecf53bcb77928d96207712d73 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 20 Dec 2024 11:19:25 +0100 Subject: [PATCH 05/23] refactor --- crates/stackable-operator/src/role_utils.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index a50670e99..23a57f134 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -368,21 +368,22 @@ where } } - pub fn merged_product_specific_common_config( + /// Returns the product specific common config from + /// 1. The role + /// 2. The role group + pub fn merged_product_specific_common_configs( &self, role_group: &str, - ) -> Result { + ) -> Result<(&ProductSpecificCommonConfig, &ProductSpecificCommonConfig), Error> { let from_role = &self.config.product_specific_common_config; - let mut merged = self + let from_role_group = &self .role_groups .get(role_group) .with_context(|| MissingRoleGroupSnafu { role_group })? .config - .product_specific_common_config - .clone(); - merged.merge(from_role); + .product_specific_common_config; - Ok(merged) + Ok((from_role, from_role_group)) } } From cc62afbd2f314181bc4dacb60dda3df0a863ee5f Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 20 Dec 2024 11:28:02 +0100 Subject: [PATCH 06/23] WIP --- crates/stackable-operator/src/role_utils.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 23a57f134..475ffdf51 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -371,10 +371,16 @@ where /// Returns the product specific common config from /// 1. The role /// 2. The role group - pub fn merged_product_specific_common_configs( - &self, + pub fn merged_product_specific_common_configs<'a>( + &'a self, role_group: &str, - ) -> Result<(&ProductSpecificCommonConfig, &ProductSpecificCommonConfig), Error> { + ) -> Result< + ( + &'a ProductSpecificCommonConfig, + &'a ProductSpecificCommonConfig, + ), + Error, + > { let from_role = &self.config.product_specific_common_config; let from_role_group = &self .role_groups From 97bde943055a4ea1a57bfaa1a36d6bba4f79b852 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Mon, 23 Dec 2024 12:25:07 +0100 Subject: [PATCH 07/23] refactor!: Use Vec instead of BTreeSet --- crates/stackable-operator/src/role_utils.rs | 78 ++++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 475ffdf51..4e31cabba 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -81,7 +81,7 @@ //! Each resource can have more operator specific labels. use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, fmt::{Debug, Display}, }; @@ -188,21 +188,18 @@ pub struct JavaCommonConfig { #[serde(rename_all = "camelCase")] pub struct JvmArgumentOverrides { #[serde(default)] - add: BTreeSet, + add: Vec, + // HashSet to be optimized for quick lookup #[serde(default)] - remove: BTreeSet, + remove: HashSet, #[serde(default)] - remove_regex: BTreeSet, + remove_regex: Vec, } impl JvmArgumentOverrides { - pub fn new( - add: BTreeSet, - remove: BTreeSet, - remove_regex: BTreeSet, - ) -> Self { + pub fn new(add: Vec, remove: HashSet, remove_regex: Vec) -> Self { Self { add, remove, @@ -210,7 +207,7 @@ impl JvmArgumentOverrides { } } - pub fn new_with_only_additions(add: BTreeSet) -> Self { + pub fn new_with_only_additions(add: Vec) -> Self { Self { add, ..Default::default() @@ -220,7 +217,7 @@ impl JvmArgumentOverrides { /// Returns all arguments that should be passed to the JVM. /// /// **Can only be called on merged config, it will panic otherwise** - pub fn effective_jvm_config_after_merging(&self) -> &BTreeSet { + pub fn effective_jvm_config_after_merging(&self) -> &Vec { assert!( self.remove.is_empty(), "After merging there should be no removals left. \"effective_jvm_config_after_merging\" should only be called on merged configs!" @@ -268,8 +265,8 @@ impl Merge for JvmArgumentOverrides { .collect(); self.add = new_add; - self.remove = BTreeSet::new(); - self.remove_regex = BTreeSet::new(); + self.remove = HashSet::new(); + self.remove_regex = Vec::new(); } } @@ -465,7 +462,7 @@ impl Display for RoleGroupRef { #[cfg(test)] mod tests { - use std::collections::BTreeSet; + use std::collections::HashSet; use crate::{config::merge::Merge, role_utils::JavaCommonConfig}; @@ -532,15 +529,15 @@ mod tests { role.merge(&operator_generated); role_group.merge(&role); - let expected = BTreeSet::from([ + let expected = Vec::from([ "-Xms34406m".to_owned(), - "-Xmx40000m".to_owned(), "-XX:+ExitOnOutOfMemoryError".to_owned(), "-Djava.protocol.handler.pkgs=sun.net.www.protocol".to_owned(), "-Dsun.net.http.allowRestrictedHeaders=true".to_owned(), "-Djava.security.properties=/stackable/nifi/conf/security.properties".to_owned(), - "-Djava.net.preferIPv4Stack=true".to_owned(), "-Dhttps.proxyHost=proxy.my.corp".to_owned(), + "-Djava.net.preferIPv4Stack=true".to_owned(), + "-Xmx40000m".to_owned(), "-Dhttps.proxyPort=1234".to_owned(), ]); @@ -549,8 +546,8 @@ mod tests { JavaCommonConfig { jvm_argument_overrides: JvmArgumentOverrides { add: expected.clone(), - remove: BTreeSet::new(), - remove_regex: BTreeSet::new() + remove: HashSet::new(), + remove_regex: Vec::new() } } ); @@ -562,4 +559,47 @@ mod tests { &expected ); } + + #[test] + fn test_merge_java_common_config_keep_order() { + let operator_generated = JavaCommonConfig { + jvm_argument_overrides: JvmArgumentOverrides::new_with_only_additions( + ["-Xms1m".to_owned()].into(), + ), + }; + + let mut role: JavaCommonConfig = serde_yaml::from_str( + r#" + jvmArgumentOverrides: + add: + - -Xms2m + "#, + ) + .unwrap(); + let mut role_group: JavaCommonConfig = serde_yaml::from_str( + r#" + jvmArgumentOverrides: + add: + - -Xms3m + "#, + ) + .unwrap(); + + // Please note that merge order is different than we normally do! + // This is not trivial, as the merge operation is not purely additive (as it is with e.g. + // PodTemplateSpec). + role.merge(&operator_generated); + role_group.merge(&role); + + assert_eq!( + role_group + .jvm_argument_overrides + .effective_jvm_config_after_merging(), + &[ + "-Xms1m".to_owned(), + "-Xms2m".to_owned(), + "-Xms3m".to_owned() + ] + ); + } } From 08759f141e28997530174a7b7921df591ee3d5d6 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 2 Jan 2025 09:46:00 +0100 Subject: [PATCH 08/23] Remove missleading function --- crates/stackable-operator/src/role_utils.rs | 35 --------------------- 1 file changed, 35 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 4e31cabba..809e38139 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -100,15 +100,8 @@ use kube::{runtime::reflector::ObjectRef, Resource}; use regex::Regex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, Snafu}; use tracing::instrument; -#[derive(Debug, Snafu)] -pub enum Error { - #[snafu(display("missing roleGroup {role_group:?}"))] - MissingRoleGroup { role_group: String }, -} - #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde( rename_all = "camelCase", @@ -364,30 +357,6 @@ where .collect(), } } - - /// Returns the product specific common config from - /// 1. The role - /// 2. The role group - pub fn merged_product_specific_common_configs<'a>( - &'a self, - role_group: &str, - ) -> Result< - ( - &'a ProductSpecificCommonConfig, - &'a ProductSpecificCommonConfig, - ), - Error, - > { - let from_role = &self.config.product_specific_common_config; - let from_role_group = &self - .role_groups - .get(role_group) - .with_context(|| MissingRoleGroupSnafu { role_group })? - .config - .product_specific_common_config; - - Ok((from_role, from_role_group)) - } } /// This is a product-agnostic RoleConfig, which is sufficient for most of the products. @@ -519,10 +488,6 @@ mod tests { ) .unwrap(); - // let mut merged = role_group; - // merged.merge(&role); - // merged.merge(&operator_generated); - // Please note that merge order is different than we normally do! // This is not trivial, as the merge operation is not purely additive (as it is with e.g. // PodTemplateSpec). From 8472dbfa6b4fff7b50a6a8d09a2fd3e7204bd85a Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 2 Jan 2025 10:56:28 +0100 Subject: [PATCH 09/23] Link to docs --- crates/stackable-operator/src/role_utils.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 809e38139..6e9e546f5 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -172,7 +172,8 @@ pub struct GenericProductSpecificCommonConfig {} pub struct JavaCommonConfig { /// Allows overriding JVM arguments. // - // TODO: Docs + /// Please read on the [JVM argument overrides documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/overrides#jvm-argument-overrides) + /// for details on the usage. #[serde(default)] pub jvm_argument_overrides: JvmArgumentOverrides, } From 5c0b3b845819a5b06609020b0c92c9affedfca67 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 2 Jan 2025 10:57:48 +0100 Subject: [PATCH 10/23] Revert "Remove missleading function" This reverts commit 08759f141e28997530174a7b7921df591ee3d5d6. --- crates/stackable-operator/src/role_utils.rs | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 6e9e546f5..c2d198dbd 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -100,8 +100,15 @@ use kube::{runtime::reflector::ObjectRef, Resource}; use regex::Regex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use snafu::{OptionExt, Snafu}; use tracing::instrument; +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("missing roleGroup {role_group:?}"))] + MissingRoleGroup { role_group: String }, +} + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde( rename_all = "camelCase", @@ -358,6 +365,30 @@ where .collect(), } } + + /// Returns the product specific common config from + /// 1. The role + /// 2. The role group + pub fn merged_product_specific_common_configs<'a>( + &'a self, + role_group: &str, + ) -> Result< + ( + &'a ProductSpecificCommonConfig, + &'a ProductSpecificCommonConfig, + ), + Error, + > { + let from_role = &self.config.product_specific_common_config; + let from_role_group = &self + .role_groups + .get(role_group) + .with_context(|| MissingRoleGroupSnafu { role_group })? + .config + .product_specific_common_config; + + Ok((from_role, from_role_group)) + } } /// This is a product-agnostic RoleConfig, which is sufficient for most of the products. @@ -489,6 +520,10 @@ mod tests { ) .unwrap(); + // let mut merged = role_group; + // merged.merge(&role); + // merged.merge(&operator_generated); + // Please note that merge order is different than we normally do! // This is not trivial, as the merge operation is not purely additive (as it is with e.g. // PodTemplateSpec). From 9a9a99c243b65ef548d72f94796f9677123d6947 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 2 Jan 2025 10:58:37 +0100 Subject: [PATCH 11/23] Rename merged_product_specific_common_configs -> get_product_specific_common_configs --- crates/stackable-operator/src/role_utils.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index c2d198dbd..4f8d78688 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -367,9 +367,10 @@ where } /// Returns the product specific common config from + /// /// 1. The role /// 2. The role group - pub fn merged_product_specific_common_configs<'a>( + pub fn get_product_specific_common_configs<'a>( &'a self, role_group: &str, ) -> Result< From b79bc64d34e2468da119ae4c77a1ad4f02028a39 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 8 Jan 2025 10:52:27 +0100 Subject: [PATCH 12/23] changelog --- crates/stackable-operator/CHANGELOG.md | 7 +++++++ crates/stackable-operator/src/role_utils.rs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 53ee7b2a2..9bfa7f6b2 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add `ProductSpecificCommonConfig`, so that products can have custom fields on the `commonConfig`. + Also add a `JavaCommonConfig`, which can be used by JVM based tools to offer `jvmArgumentOverrides` ([#931]) + +[#931]: https://github.com/stackabletech/operator-rs/pull/931 + ## [0.83.0] - 2024-12-03 ### Added diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 4f8d78688..2f936b2ed 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -159,7 +159,7 @@ pub struct CommonConfiguration { // No docs needed, as we flatten this struct. // // This field is product-specific and can contain e.g. jvmArgumentOverrides. - // It is not accessible by operators, please use to read the value + // It is not accessible by operators, please use [`Role::get_product_specific_common_configs`] to read the values. #[serde(flatten, default)] pub(crate) product_specific_common_config: ProductSpecificCommonConfig, } From 670dfb4755ca3b06cc6b5961864fe9b841340b44 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 8 Jan 2025 10:53:13 +0100 Subject: [PATCH 13/23] typo --- crates/stackable-operator/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 9bfa7f6b2..3dd721738 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### Added -- Add `ProductSpecificCommonConfig`, so that products can have custom fields on the `commonConfig`. +- Add `ProductSpecificCommonConfig`, so that product operators can have custom fields on the `commonConfig`. Also add a `JavaCommonConfig`, which can be used by JVM based tools to offer `jvmArgumentOverrides` ([#931]) [#931]: https://github.com/stackabletech/operator-rs/pull/931 From c26e533d8cd67870d5aa5a0b8c4aca845c3a0488 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 8 Jan 2025 10:56:01 +0100 Subject: [PATCH 14/23] Improve changelog --- crates/stackable-operator/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 3dd721738..cf3883f24 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -6,8 +6,8 @@ All notable changes to this project will be documented in this file. ### Added -- Add `ProductSpecificCommonConfig`, so that product operators can have custom fields on the `commonConfig`. - Also add a `JavaCommonConfig`, which can be used by JVM based tools to offer `jvmArgumentOverrides` ([#931]) +- Add `ProductSpecificCommonConfig`, so that product operators can have custom fields within `commonConfig`. + Also add a `JavaCommonConfig`, which can be used by JVM-based tools to offer `jvmArgumentOverrides` with this mechanism ([#931]) [#931]: https://github.com/stackabletech/operator-rs/pull/931 From c6cae7019967de2b3fac99b32eb2eef17aa6346c Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 8 Jan 2025 11:04:47 +0100 Subject: [PATCH 15/23] Add some CRD docs --- crates/stackable-operator/src/role_utils.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 2f936b2ed..206306f9c 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -188,13 +188,17 @@ pub struct JavaCommonConfig { #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct JvmArgumentOverrides { + /// JVM arguments to be added #[serde(default)] add: Vec, + /// JVM arguments to be removed by exact match + // // HashSet to be optimized for quick lookup #[serde(default)] remove: HashSet, + /// JVM arguments matching any of this regexes will be removed #[serde(default)] remove_regex: Vec, } From 3ebcc828c872e892ee158e9d5b28beb67564c337 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 8 Jan 2025 11:25:23 +0100 Subject: [PATCH 16/23] Improve rustdoc --- crates/stackable-operator/src/role_utils.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 206306f9c..ea5d1fedf 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -219,9 +219,12 @@ impl JvmArgumentOverrides { } } - /// Returns all arguments that should be passed to the JVM. + /// Called on **merged** [`JvmArgumentOverrides`}, returns all arguments that should be passed to the JVM. /// - /// **Can only be called on merged config, it will panic otherwise** + /// **Can only be called on merged config, it will panic otherwise!** + /// + /// We are panicking (instead of returning an Error), because this is not the users fault, but + /// the operator is doing things wrong pub fn effective_jvm_config_after_merging(&self) -> &Vec { assert!( self.remove.is_empty(), From de5d58045b78668f18da1a798771780fcd5d96d1 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 8 Jan 2025 11:28:23 +0100 Subject: [PATCH 17/23] Add some rustdoc --- crates/stackable-operator/src/role_utils.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index ea5d1fedf..11c14b659 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -240,6 +240,8 @@ impl JvmArgumentOverrides { } impl Merge for JvmArgumentOverrides { + /// Please watch out: Merge order is complicated for this merge. + /// Test your code! #[instrument] fn merge(&mut self, defaults: &Self) { let regexes = self From 57f8f4e008ab17f8639c3e1e8ab8410f85d1a58c Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Tue, 14 Jan 2025 09:28:54 +0100 Subject: [PATCH 18/23] changelog --- crates/stackable-operator/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 680daa14d..fc65f9ef3 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -6,7 +6,6 @@ All notable changes to this project will be documented in this file. ### Added - - BREAKING: Aggregate emitted Kubernetes events on the CustomResources thanks to the new [kube feature](https://github.com/kube-rs/controller-rs/pull/116). Instead of reporting the same event multiple times it now uses `EventSeries` to aggregate these events to single entry with an From 66d4ec9f7965869c1546453f31dfd4a3e53d2241 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Tue, 14 Jan 2025 11:27:15 +0100 Subject: [PATCH 19/23] refactor!: Dont use Merge, implement try_merge instead --- crates/stackable-operator/src/role_utils.rs | 203 +++++++++++--------- 1 file changed, 114 insertions(+), 89 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 11c14b659..bf19fec03 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -100,13 +100,17 @@ use kube::{runtime::reflector::ObjectRef, Resource}; use regex::Regex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, Snafu}; -use tracing::instrument; +use snafu::{OptionExt, ResultExt, Snafu}; #[derive(Debug, Snafu)] pub enum Error { #[snafu(display("missing roleGroup {role_group:?}"))] MissingRoleGroup { role_group: String }, + + #[snafu(display( + "Could not parse regex from \"jvmArgumentOverrides.removeRegex\", ignoring it (there might be some added anchors at the start and end): {regex:?}" + ))] + InvalidRemoveRegex { source: regex::Error, regex: String }, } #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] @@ -173,8 +177,7 @@ impl CommonConfiguration Result<(), Error> { let regexes = self .remove_regex .iter() - .filter_map(|regex| { + .map(|regex| { let without_anchors = regex.trim_start_matches('^').trim_end_matches('$'); let with_anchors = format!("^{without_anchors}$"); - match Regex::new(&with_anchors) { - Ok(regex) => Some(regex), - Err(error) => { - tracing::warn!( - regex, - regex_without_anchors = without_anchors, - regex_with_anchors = with_anchors, - ?error, - "Could not parse regex from \"jvmArgumentOverrides.removeRegex\", ignoring it" - ); - None - } - }}) - .collect::>(); + + Regex::new(&with_anchors).with_context(|_| InvalidRemoveRegexSnafu { + regex: with_anchors, + }) + }) + .collect::, Error>>()?; let new_add = defaults .add @@ -277,6 +272,8 @@ impl Merge for JvmArgumentOverrides { self.add = new_add; self.remove = HashSet::new(); self.remove_regex = Vec::new(); + + Ok(()) } } @@ -374,30 +371,42 @@ where .collect(), } } +} - /// Returns the product specific common config from +impl Role +where + U: Default + JsonSchema + Serialize, +{ + /// Merges jvm argument overrides from /// - /// 1. The role - /// 2. The role group - pub fn get_product_specific_common_configs<'a>( - &'a self, + /// 1. It takes the operator generated JVM args + /// 2. It applies role level overrides + /// 3. It applies roleGroup level overrides + pub fn get_merged_jvm_argument_overrides( + &self, role_group: &str, - ) -> Result< - ( - &'a ProductSpecificCommonConfig, - &'a ProductSpecificCommonConfig, - ), - Error, - > { - let from_role = &self.config.product_specific_common_config; + operator_generated: &JvmArgumentOverrides, + ) -> Result { + let from_role = &self + .config + .product_specific_common_config + .jvm_argument_overrides; let from_role_group = &self .role_groups .get(role_group) .with_context(|| MissingRoleGroupSnafu { role_group })? .config - .product_specific_common_config; + .product_specific_common_config + .jvm_argument_overrides; + + // Please note that the merge order is different than we normally do! + // This is not trivial, as the merge operation is not purely additive (as it is with e.g. `PodTemplateSpec). + let mut from_role = from_role.clone(); + from_role.try_merge(&operator_generated)?; + let mut from_role_group = from_role_group.clone(); + from_role_group.try_merge(&from_role)?; - Ok((from_role, from_role_group)) + Ok(from_role_group) } } @@ -475,32 +484,29 @@ impl Display for RoleGroupRef { mod tests { use std::collections::HashSet; - use crate::{config::merge::Merge, role_utils::JavaCommonConfig}; + use crate::role_utils::JavaCommonConfig; - use super::JvmArgumentOverrides; + use super::*; #[test] fn test_merge_java_common_config() { // The operator generates some JVM arguments - let operator_generated = JavaCommonConfig { - jvm_argument_overrides: JvmArgumentOverrides::new_with_only_additions( - [ - "-Xms34406m".to_owned(), - "-Xmx34406m".to_owned(), - "-XX:+UseG1GC".to_owned(), - "-XX:+ExitOnOutOfMemoryError".to_owned(), - "-Djava.protocol.handler.pkgs=sun.net.www.protocol".to_owned(), - "-Dsun.net.http.allowRestrictedHeaders=true".to_owned(), - "-Djava.security.properties=/stackable/nifi/conf/security.properties" - .to_owned(), - ] - .into(), - ), - }; + let operator_generated = JvmArgumentOverrides::new_with_only_additions( + [ + "-Xms34406m".to_owned(), + "-Xmx34406m".to_owned(), + "-XX:+UseG1GC".to_owned(), + "-XX:+ExitOnOutOfMemoryError".to_owned(), + "-Djava.protocol.handler.pkgs=sun.net.www.protocol".to_owned(), + "-Dsun.net.http.allowRestrictedHeaders=true".to_owned(), + "-Djava.security.properties=/stackable/nifi/conf/security.properties".to_owned(), + ] + .into(), + ); // 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... - let mut role: JavaCommonConfig = serde_yaml::from_str( + let role: JavaCommonConfig = serde_yaml::from_str( r#" jvmArgumentOverrides: remove: @@ -516,7 +522,7 @@ mod tests { // For the roleGroup, let's say we need a different memory config. // For that to work we first remove the flags generated by the operator and add our own. // Also we override the proxy port to test that the roleGroup config takes precedence over the role config. - let mut role_group: JavaCommonConfig = serde_yaml::from_str( + let role_group: JavaCommonConfig = serde_yaml::from_str( r#" jvmArgumentOverrides: removeRegex: @@ -530,15 +536,27 @@ mod tests { ) .unwrap(); - // let mut merged = role_group; - // merged.merge(&role); - // merged.merge(&operator_generated); + let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = Role { + config: CommonConfiguration { + product_specific_common_config: role, + ..Default::default() + }, + role_groups: HashMap::from([( + "default".to_owned(), + RoleGroup { + config: CommonConfiguration { + product_specific_common_config: role_group, + ..Default::default() + }, + replicas: Some(42), + }, + )]), + ..Default::default() + }; - // Please note that merge order is different than we normally do! - // This is not trivial, as the merge operation is not purely additive (as it is with e.g. - // PodTemplateSpec). - role.merge(&operator_generated); - role_group.merge(&role); + let merged_jvm_argument_overrides = entire_role + .get_merged_jvm_argument_overrides("default", &operator_generated) + .expect("Failed to merge jvm argument overrides"); let expected = Vec::from([ "-Xms34406m".to_owned(), @@ -553,33 +571,26 @@ mod tests { ]); assert_eq!( - role_group, - JavaCommonConfig { - jvm_argument_overrides: JvmArgumentOverrides { - add: expected.clone(), - remove: HashSet::new(), - remove_regex: Vec::new() - } + merged_jvm_argument_overrides, + JvmArgumentOverrides { + add: expected.clone(), + remove: HashSet::new(), + remove_regex: Vec::new() } ); assert_eq!( - role_group - .jvm_argument_overrides - .effective_jvm_config_after_merging(), + merged_jvm_argument_overrides.effective_jvm_config_after_merging(), &expected ); } #[test] fn test_merge_java_common_config_keep_order() { - let operator_generated = JavaCommonConfig { - jvm_argument_overrides: JvmArgumentOverrides::new_with_only_additions( - ["-Xms1m".to_owned()].into(), - ), - }; + let operator_generated = + JvmArgumentOverrides::new_with_only_additions(["-Xms1m".to_owned()].into()); - let mut role: JavaCommonConfig = serde_yaml::from_str( + let role: JavaCommonConfig = serde_yaml::from_str( r#" jvmArgumentOverrides: add: @@ -587,7 +598,7 @@ mod tests { "#, ) .unwrap(); - let mut role_group: JavaCommonConfig = serde_yaml::from_str( + let role_group: JavaCommonConfig = serde_yaml::from_str( r#" jvmArgumentOverrides: add: @@ -596,16 +607,30 @@ mod tests { ) .unwrap(); - // Please note that merge order is different than we normally do! - // This is not trivial, as the merge operation is not purely additive (as it is with e.g. - // PodTemplateSpec). - role.merge(&operator_generated); - role_group.merge(&role); + let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = Role { + config: CommonConfiguration { + product_specific_common_config: role, + ..Default::default() + }, + role_groups: HashMap::from([( + "default".to_owned(), + RoleGroup { + config: CommonConfiguration { + product_specific_common_config: role_group, + ..Default::default() + }, + replicas: Some(42), + }, + )]), + ..Default::default() + }; + + let merged_jvm_argument_overrides = entire_role + .get_merged_jvm_argument_overrides("default", &operator_generated) + .expect("Failed to merge jvm argument overrides"); assert_eq!( - role_group - .jvm_argument_overrides - .effective_jvm_config_after_merging(), + merged_jvm_argument_overrides.effective_jvm_config_after_merging(), &[ "-Xms1m".to_owned(), "-Xms2m".to_owned(), From c816bab018280f0b69285dff8a5e942b84b7cd8a Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Tue, 14 Jan 2025 11:58:31 +0100 Subject: [PATCH 20/23] clippy --- crates/stackable-operator/src/role_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index bf19fec03..bd7f81c31 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -402,7 +402,7 @@ where // Please note that the merge order is different than we normally do! // This is not trivial, as the merge operation is not purely additive (as it is with e.g. `PodTemplateSpec). let mut from_role = from_role.clone(); - from_role.try_merge(&operator_generated)?; + from_role.try_merge(operator_generated)?; let mut from_role_group = from_role_group.clone(); from_role_group.try_merge(&from_role)?; From e3151e3f3b542a863bd4d381889bd5834fbed62a Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 15 Jan 2025 08:32:26 +0100 Subject: [PATCH 21/23] Use serde_yaml for entire_role --- crates/stackable-operator/src/role_utils.rs | 62 +++++++-------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index bd7f81c31..57b86335d 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -504,55 +504,31 @@ mod tests { .into(), ); - // 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... - let role: JavaCommonConfig = serde_yaml::from_str( - r#" + let entire_role: Role<(), 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... jvmArgumentOverrides: remove: - -XX:+UseG1GC - add: + add: # Add some networking arguments - -Dhttps.proxyHost=proxy.my.corp - -Dhttps.proxyPort=8080 - -Djava.net.preferIPv4Stack=true - "#, - ) - .unwrap(); - - // For the roleGroup, let's say we need a different memory config. - // For that to work we first remove the flags generated by the operator and add our own. - // Also we override the proxy port to test that the roleGroup config takes precedence over the role config. - let role_group: JavaCommonConfig = serde_yaml::from_str( - r#" - jvmArgumentOverrides: - removeRegex: - - # We need more memory! - - -Xmx.* - - -Dhttps.proxyPort=.* - add: - - -Xmx40000m - - -Dhttps.proxyPort=1234 - "#, - ) - .unwrap(); - - let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = Role { - config: CommonConfiguration { - product_specific_common_config: role, - ..Default::default() - }, - role_groups: HashMap::from([( - "default".to_owned(), - RoleGroup { - config: CommonConfiguration { - product_specific_common_config: role_group, - ..Default::default() - }, - replicas: Some(42), - }, - )]), - ..Default::default() - }; + roleGroups: + default: + # For the roleGroup, let's say we need a different memory config. + # For that to work we first remove the flags generated by the operator and add our own. + # Also we override the proxy port to test that the roleGroup config takes precedence over the role config. + jvmArgumentOverrides: + removeRegex: + - -Xmx.* + - -Dhttps.proxyPort=.* + add: + - -Xmx40000m + - -Dhttps.proxyPort=1234 + ") + .expect("Failed to parse role"); let merged_jvm_argument_overrides = entire_role .get_merged_jvm_argument_overrides("default", &operator_generated) From ddb503a54d248af5df03d7613542a3ba54dab338 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 15 Jan 2025 09:30:23 +0100 Subject: [PATCH 22/23] simplyfy merge_java_common_config_keep_order test --- crates/stackable-operator/src/role_utils.rs | 41 +++++---------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index 57b86335d..f7f2d4787 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -566,40 +566,19 @@ mod tests { let operator_generated = JvmArgumentOverrides::new_with_only_additions(["-Xms1m".to_owned()].into()); - let role: JavaCommonConfig = serde_yaml::from_str( - r#" + let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = serde_yaml::from_str( + " jvmArgumentOverrides: add: - - -Xms2m - "#, - ) - .unwrap(); - let role_group: JavaCommonConfig = serde_yaml::from_str( - r#" - jvmArgumentOverrides: - add: - - -Xms3m - "#, + - -Xms2m + roleGroups: + default: + jvmArgumentOverrides: + add: + - -Xms3m + ", ) - .unwrap(); - - let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = Role { - config: CommonConfiguration { - product_specific_common_config: role, - ..Default::default() - }, - role_groups: HashMap::from([( - "default".to_owned(), - RoleGroup { - config: CommonConfiguration { - product_specific_common_config: role_group, - ..Default::default() - }, - replicas: Some(42), - }, - )]), - ..Default::default() - }; + .expect("Failed to parse role"); let merged_jvm_argument_overrides = entire_role .get_merged_jvm_argument_overrides("default", &operator_generated) From 5769f5c3b64e450b6cacec0478a7c4e94bf8347c Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 15 Jan 2025 09:33:36 +0100 Subject: [PATCH 23/23] Update crates/stackable-operator/src/role_utils.rs Co-authored-by: Sebastian Bernauer --- crates/stackable-operator/src/role_utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/stackable-operator/src/role_utils.rs b/crates/stackable-operator/src/role_utils.rs index f7f2d4787..e77672793 100644 --- a/crates/stackable-operator/src/role_utils.rs +++ b/crates/stackable-operator/src/role_utils.rs @@ -569,8 +569,8 @@ mod tests { let entire_role: Role<(), GenericRoleConfig, JavaCommonConfig> = serde_yaml::from_str( " jvmArgumentOverrides: - add: - - -Xms2m + add: + - -Xms2m roleGroups: default: jvmArgumentOverrides: