diff --git a/CHANGELOG.md b/CHANGELOG.md index 765c2230..895e788d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ `hbase-env.sh`, `ssl-server.xml`, `ssl-client.xml` and `security.properties`). Previously, arbitrary file names were silently accepted and ignored ([#751]). - Bump `stackable-operator` to 0.111.1 and snafu to 0.9 ([#751], [#752]). +- Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#757]). [#745]: https://github.com/stackabletech/hbase-operator/pull/745 [#751]: https://github.com/stackabletech/hbase-operator/pull/751 [#752]: https://github.com/stackabletech/hbase-operator/pull/752 +[#757]: https://github.com/stackabletech/hbase-operator/pull/757 ## [26.3.0] - 2026-03-16 diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs new file mode 100644 index 00000000..78615bd9 --- /dev/null +++ b/rust/operator-binary/src/controller/dereference.rs @@ -0,0 +1,43 @@ +use snafu::{ResultExt, Snafu}; + +use crate::{ + crd::v1alpha1, security::opa::HbaseOpaConfig, zookeeper::ZookeeperConnectionInformation, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to retrieve zookeeper connection information"))] + RetrieveZookeeperConnectionInformation { source: crate::zookeeper::Error }, + + #[snafu(display("invalid OPA configuration"))] + InvalidOpaConfig { source: crate::security::opa::Error }, +} + +/// External references resolved during the dereference step. +pub struct DereferencedObjects { + pub zookeeper_connection_information: ZookeeperConnectionInformation, + pub hbase_opa_config: Option, +} + +pub async fn dereference( + client: &stackable_operator::client::Client, + hbase: &v1alpha1::HbaseCluster, +) -> Result { + let zookeeper_connection_information = ZookeeperConnectionInformation::retrieve(hbase, client) + .await + .context(RetrieveZookeeperConnectionInformationSnafu)?; + + let hbase_opa_config = match &hbase.spec.cluster_config.authorization { + Some(opa_config) => Some( + HbaseOpaConfig::from_opa_config(client, hbase, opa_config) + .await + .context(InvalidOpaConfigSnafu)?, + ), + None => None, + }; + + Ok(DereferencedObjects { + zookeeper_connection_information, + hbase_opa_config, + }) +} diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs new file mode 100644 index 00000000..1b261dfe --- /dev/null +++ b/rust/operator-binary/src/controller/mod.rs @@ -0,0 +1,2 @@ +pub mod dereference; +pub mod validate; diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs new file mode 100644 index 00000000..685508b5 --- /dev/null +++ b/rust/operator-binary/src/controller/validate.rs @@ -0,0 +1,136 @@ +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; + +use product_config::{ProductConfigManager, types::PropertyNameKind}; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + commons::product_image_selection::{self, ResolvedProductImage}, + product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, + role_utils::GenericRoleConfig, +}; + +use crate::crd::{AnyServiceConfig, HbaseRole, v1alpha1}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to resolve product image"))] + ResolveProductImage { + source: product_image_selection::Error, + }, + + #[snafu(display("invalid role properties"))] + RoleProperties { source: crate::crd::Error }, + + #[snafu(display("failed to generate product config"))] + GenerateProductConfig { + source: stackable_operator::product_config_utils::Error, + }, + + #[snafu(display("invalid product config"))] + InvalidProductConfig { + source: stackable_operator::product_config_utils::Error, + }, + + #[snafu(display("could not parse Hbase role [{role}]"))] + UnidentifiedHbaseRole { + source: strum::ParseError, + role: String, + }, + + #[snafu(display("failed to resolve and merge config for role and role group"))] + FailedToResolveConfig { source: crate::crd::Error }, +} + +/// Per-role configuration extracted during validation. +#[derive(Clone, Debug)] +pub struct ValidatedRoleConfig { + pub pdb: stackable_operator::commons::pdb::PdbConfig, +} + +/// Per-rolegroup configuration: the merged CRD config plus the product-config properties. +#[derive(Clone, Debug)] +pub struct ValidatedRoleGroupConfig { + pub merged_config: AnyServiceConfig, + pub product_config_properties: HashMap>, +} + +/// The validated cluster: proves that product-config validation and config merging +/// succeeded for every role and role group before any resources are created. +#[derive(Clone, Debug)] +pub struct ValidatedHbaseCluster { + pub image: ResolvedProductImage, + pub role_groups: BTreeMap>, + pub role_configs: BTreeMap, +} + +pub fn validate_cluster( + hbase: &v1alpha1::HbaseCluster, + image_base_name: &str, + image_repository: &str, + pkg_version: &str, + product_config_manager: &ProductConfigManager, +) -> Result { + let resolved_product_image = hbase + .spec + .image + .resolve(image_base_name, image_repository, pkg_version) + .context(ResolveProductImageSnafu)?; + + let roles = hbase.build_role_properties().context(RolePropertiesSnafu)?; + + let validated_config = validate_all_roles_and_groups_config( + &resolved_product_image.product_version, + &transform_all_roles_to_config(hbase, &roles).context(GenerateProductConfigSnafu)?, + product_config_manager, + false, + false, + ) + .context(InvalidProductConfigSnafu)?; + + let mut role_groups = BTreeMap::new(); + let mut role_configs = BTreeMap::new(); + + for (role_name, group_config) in validated_config.iter() { + let hbase_role = HbaseRole::from_str(role_name).context(UnidentifiedHbaseRoleSnafu { + role: role_name.to_string(), + })?; + + if let Some(GenericRoleConfig { + pod_disruption_budget: pdb, + }) = hbase.role_config(&hbase_role) + { + role_configs.insert(hbase_role.clone(), ValidatedRoleConfig { pdb: pdb.clone() }); + } + + let mut group_configs = BTreeMap::new(); + for (rolegroup_name, rolegroup_config) in group_config.iter() { + let rolegroup = hbase.server_rolegroup_ref(role_name, rolegroup_name); + + let merged_config = hbase + .merged_config( + &hbase_role, + &rolegroup.role_group, + &hbase.spec.cluster_config.hdfs_config_map_name, + ) + .context(FailedToResolveConfigSnafu)?; + + group_configs.insert( + rolegroup_name.clone(), + ValidatedRoleGroupConfig { + merged_config, + product_config_properties: rolegroup_config.clone(), + }, + ); + } + + role_groups.insert(hbase_role, group_configs); + } + + Ok(ValidatedHbaseCluster { + image: resolved_product_image, + role_groups, + role_configs, + }) +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 07d7a984..80b0707a 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -623,7 +623,9 @@ pub fn merged_env(rolegroup_config: Option<&BTreeMap>) -> Vec, } +#[derive(Clone, Debug)] pub enum AnyServiceConfig { Master(HbaseConfig), RegionServer(RegionServerConfig), diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index 9d81d080..fa2caa73 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -27,10 +27,7 @@ use stackable_operator::{ }, cli::OperatorEnvironmentOptions, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::{ - product_image_selection::{self, ResolvedProductImage}, - rbac::build_rbac_resources, - }, + commons::{product_image_selection::ResolvedProductImage, rbac::build_rbac_resources}, constants::RESTART_CONTROLLER_ENABLED_LABEL, k8s_openapi::{ api::{ @@ -50,7 +47,6 @@ use stackable_operator::{ kvp::{Annotations, Label, LabelError, Labels, ObjectLabels}, logging::controller::ReconcilerError, memory::{BinaryMultiple, MemoryQuantity}, - product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, product_logging::{ self, framework::LoggingError, @@ -59,7 +55,7 @@ use stackable_operator::{ CustomContainerLogConfig, }, }, - role_utils::{GenericRoleConfig, RoleGroupRef}, + role_utils::RoleGroupRef, shared::time::Duration, status::condition::{ compute_conditions, operations::ClusterOperationsConditionBuilder, @@ -90,8 +86,8 @@ use crate::{ product_logging::{ CONTAINERDEBUG_LOG_DIRECTORY, STACKABLE_LOG_DIR, extend_role_group_config_map, }, - security::{self, opa::HbaseOpaConfig}, - zookeeper::{self, ZookeeperConnectionInformation}, + security::opa::HbaseOpaConfig, + zookeeper::ZookeeperConnectionInformation, }; pub const HBASE_CONTROLLER_NAME: &str = "hbasecluster"; @@ -118,9 +114,6 @@ pub struct Ctx { #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { - #[snafu(display("invalid role properties"))] - RoleProperties { source: crate::crd::Error }, - #[snafu(display("missing secret lifetime"))] MissingSecretLifetime, @@ -166,19 +159,6 @@ pub enum Error { rolegroup: RoleGroupRef, }, - #[snafu(display("failed to generate product config"))] - GenerateProductConfig { - source: stackable_operator::product_config_utils::Error, - }, - - #[snafu(display("invalid product config"))] - InvalidProductConfig { - source: stackable_operator::product_config_utils::Error, - }, - - #[snafu(display("failed to retrieve zookeeper connection information"))] - RetrieveZookeeperConnectionInformation { source: zookeeper::Error }, - #[snafu(display("object is missing metadata to build owner reference"))] ObjectMissingMetadataForOwnerRef { source: stackable_operator::builder::meta::Error, @@ -200,15 +180,6 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("could not parse Hbase role [{role}]"))] - UnidentifiedHbaseRole { - source: strum::ParseError, - role: String, - }, - - #[snafu(display("failed to resolve and merge config for role and role group"))] - FailedToResolveConfig { source: crate::crd::Error }, - #[snafu(display("vector agent is enabled but vector aggregator ConfigMap is missing"))] VectorAggregatorConfigMapMissing, @@ -258,9 +229,6 @@ pub enum Error { source: stackable_operator::builder::meta::Error, }, - #[snafu(display("invalid OPA configuration"))] - InvalidOpaConfig { source: security::opa::Error }, - #[snafu(display("unknown role [{role}]"))] UnknownHbaseRole { source: ParseError, role: String }, @@ -297,9 +265,14 @@ pub enum Error { #[snafu(display("failed to build listener persistent volume claim"))] ListenerPersistentVolumeClaim { source: crate::crd::Error }, - #[snafu(display("failed to resolve product image"))] - ResolveProductImage { - source: product_image_selection::Error, + #[snafu(display("failed to dereference cluster resources"))] + Dereference { + source: crate::controller::dereference::Error, + }, + + #[snafu(display("failed to validate cluster configuration"))] + Validate { + source: crate::controller::validate::Error, }, } @@ -325,39 +298,18 @@ pub async fn reconcile_hbase( let client = &ctx.client; - let resolved_product_image = hbase - .spec - .image - .resolve( - CONTAINER_IMAGE_BASE_NAME, - &ctx.operator_environment.image_repository, - crate::built_info::PKG_VERSION, - ) - .context(ResolveProductImageSnafu)?; - let zookeeper_connection_information = ZookeeperConnectionInformation::retrieve(hbase, client) + let dereferenced = crate::controller::dereference::dereference(client, hbase) .await - .context(RetrieveZookeeperConnectionInformationSnafu)?; + .context(DereferenceSnafu)?; - let validated_config = { - let roles = hbase.build_role_properties().context(RolePropertiesSnafu)?; - validate_all_roles_and_groups_config( - &resolved_product_image.app_version_label_value, - &transform_all_roles_to_config(hbase, &roles).context(GenerateProductConfigSnafu)?, - &ctx.product_config, - false, - false, - ) - .context(InvalidProductConfigSnafu)? - }; - - let hbase_opa_config = match &hbase.spec.cluster_config.authorization { - Some(opa_config) => Some( - HbaseOpaConfig::from_opa_config(client, hbase, opa_config) - .await - .context(InvalidOpaConfigSnafu)?, - ), - None => None, - }; + let validated = crate::controller::validate::validate_cluster( + hbase, + CONTAINER_IMAGE_BASE_NAME, + &ctx.operator_environment.image_repository, + crate::built_info::PKG_VERSION, + &ctx.product_config, + ) + .context(ValidateSnafu)?; let mut cluster_resources = ClusterResources::new( APP_NAME, @@ -388,48 +340,33 @@ pub async fn reconcile_hbase( let mut ss_cond_builder = StatefulSetConditionBuilder::default(); - for (role_name, group_config) in validated_config.iter() { - let hbase_role = HbaseRole::from_str(role_name).context(UnidentifiedHbaseRoleSnafu { - role: role_name.to_string(), - })?; - for (rolegroup_name, rolegroup_config) in group_config.iter() { - let rolegroup = hbase.server_rolegroup_ref(role_name, rolegroup_name); - - let merged_config = hbase - .merged_config( - &hbase_role, - &rolegroup.role_group, - &hbase.spec.cluster_config.hdfs_config_map_name, - ) - .context(FailedToResolveConfigSnafu)?; + for (hbase_role, role_group_configs) in &validated.role_groups { + for (rolegroup_name, validated_rg_config) in role_group_configs { + let rolegroup = hbase.server_rolegroup_ref(hbase_role.to_string(), rolegroup_name); let rg_service = - build_rolegroup_service(hbase, &hbase_role, &rolegroup, &resolved_product_image)?; + build_rolegroup_service(hbase, hbase_role, &rolegroup, &validated.image)?; - let rg_metrics_service = build_rolegroup_metrics_service( - hbase, - &hbase_role, - &rolegroup, - &resolved_product_image, - )?; + let rg_metrics_service = + build_rolegroup_metrics_service(hbase, hbase_role, &rolegroup, &validated.image)?; let rg_configmap = build_rolegroup_config_map( hbase, &client.kubernetes_cluster_info, &rolegroup, - rolegroup_config, - &zookeeper_connection_information, - &merged_config, - &resolved_product_image, - hbase_opa_config.as_ref(), + &validated_rg_config.product_config_properties, + &dereferenced.zookeeper_connection_information, + &validated_rg_config.merged_config, + &validated.image, + dereferenced.hbase_opa_config.as_ref(), )?; let rg_statefulset = build_rolegroup_statefulset( hbase, - &hbase_role, + hbase_role, &rolegroup, - rolegroup_config, - &merged_config, - &resolved_product_image, + &validated_rg_config.product_config_properties, + &validated_rg_config.merged_config, + &validated.image, &rbac_sa, )?; cluster_resources @@ -464,14 +401,16 @@ pub async fn reconcile_hbase( ); } - let role_config = hbase.role_config(&hbase_role); - if let Some(GenericRoleConfig { - pod_disruption_budget: pdb, - }) = role_config - { - add_pdbs(pdb, hbase, &hbase_role, client, &mut cluster_resources) - .await - .context(FailedToCreatePdbSnafu)?; + if let Some(role_config) = validated.role_configs.get(hbase_role) { + add_pdbs( + &role_config.pdb, + hbase, + hbase_role, + client, + &mut cluster_resources, + ) + .await + .context(FailedToCreatePdbSnafu)?; } } @@ -480,8 +419,8 @@ pub async fn reconcile_hbase( let discovery_cm = build_discovery_configmap( hbase, &client.kubernetes_cluster_info, - &zookeeper_connection_information, - &resolved_product_image, + &dereferenced.zookeeper_connection_information, + &validated.image, ) .context(BuildDiscoveryConfigMapSnafu)?; cluster_resources diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 63f9334a..917c95fd 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -38,6 +38,7 @@ use crate::{ }; mod config; +mod controller; mod crd; mod discovery; mod hbase_controller;