From 05ba65ffdf3b03d49bedc91dd0a06a8c745a77d1 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 12 May 2026 18:00:49 +0200 Subject: [PATCH 01/14] refactor: extract dereference and validate steps into controller modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move external-reference resolution (product image, ZooKeeper, OPA) into controller/dereference.rs and product-config validation with config merging into controller/validate.rs. The reconciler now calls dereference → validate as an explicit pipeline, matching the pattern established in the airflow and hive operators. Co-Authored-By: Claude Opus 4.6 --- .../src/controller/dereference.rs | 60 +++++++ rust/operator-binary/src/controller/mod.rs | 2 + .../src/controller/validate.rs | 124 ++++++++++++++ rust/operator-binary/src/crd/mod.rs | 3 + rust/operator-binary/src/hbase_controller.rs | 162 ++++++------------ rust/operator-binary/src/main.rs | 1 + 6 files changed, 241 insertions(+), 111 deletions(-) create mode 100644 rust/operator-binary/src/controller/dereference.rs create mode 100644 rust/operator-binary/src/controller/mod.rs create mode 100644 rust/operator-binary/src/controller/validate.rs diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs new file mode 100644 index 00000000..b48f26a3 --- /dev/null +++ b/rust/operator-binary/src/controller/dereference.rs @@ -0,0 +1,60 @@ +use snafu::{ResultExt, Snafu}; +use stackable_operator::commons::product_image_selection::{self, ResolvedProductImage}; + +use crate::{ + crd::v1alpha1, security::opa::HbaseOpaConfig, zookeeper::ZookeeperConnectionInformation, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to resolve product image"))] + ResolveProductImage { + source: product_image_selection::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 resolved_product_image: ResolvedProductImage, + pub zookeeper_connection_information: ZookeeperConnectionInformation, + pub hbase_opa_config: Option, +} + +pub async fn dereference( + client: &stackable_operator::client::Client, + hbase: &v1alpha1::HbaseCluster, + image_base_name: &str, + image_repository: &str, + pkg_version: &str, +) -> Result { + let resolved_product_image = hbase + .spec + .image + .resolve(image_base_name, image_repository, pkg_version) + .context(ResolveProductImageSnafu)?; + + 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 { + resolved_product_image, + 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..63a1c986 --- /dev/null +++ b/rust/operator-binary/src/controller/validate.rs @@ -0,0 +1,124 @@ +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; + +use product_config::{ProductConfigManager, types::PropertyNameKind}; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + commons::product_image_selection::ResolvedProductImage, + product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, + role_utils::GenericRoleConfig, +}; + +use super::dereference::DereferencedObjects; +use crate::crd::{AnyServiceConfig, HbaseRole, v1alpha1}; + +#[derive(Snafu, Debug)] +pub enum 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, + dereferenced: &DereferencedObjects, + product_config_manager: &ProductConfigManager, +) -> Result { + let roles = hbase.build_role_properties().context(RolePropertiesSnafu)?; + + let validated_config = validate_all_roles_and_groups_config( + &dereferenced.resolved_product_image.app_version_label_value, + &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: dereferenced.resolved_product_image.clone(), + 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..a7528307 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,19 @@ 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) - .await - .context(RetrieveZookeeperConnectionInformationSnafu)?; - - 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 dereferenced = crate::controller::dereference::dereference( + client, + hbase, + CONTAINER_IMAGE_BASE_NAME, + &ctx.operator_environment.image_repository, + crate::built_info::PKG_VERSION, + ) + .await + .context(DereferenceSnafu)?; - 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, &dereferenced, &ctx.product_config) + .context(ValidateSnafu)?; let mut cluster_resources = ClusterResources::new( APP_NAME, @@ -388,48 +341,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 +402,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 +420,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; From d1b35fc43a917d916e7b345753d011dd2ed8537d Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 13 May 2026 12:00:24 +0200 Subject: [PATCH 02/14] fix: pass product_version instead of app_version_label_value to product config validation The label-safe composite version string (e.g. "2.6.4-stackable24.7.0") is not suitable for semver matching against fromVersion/asOfVersion constraints in properties.yaml. Use the raw product_version consistently with all other operators. Co-Authored-By: Claude Opus 4.6 --- rust/operator-binary/src/controller/validate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 63a1c986..947f0176 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -69,7 +69,7 @@ pub fn validate_cluster( let roles = hbase.build_role_properties().context(RolePropertiesSnafu)?; let validated_config = validate_all_roles_and_groups_config( - &dereferenced.resolved_product_image.app_version_label_value, + &dereferenced.resolved_product_image.product_version, &transform_all_roles_to_config(hbase, &roles).context(GenerateProductConfigSnafu)?, product_config_manager, false, From ad56b8512d41a97662e5289a96ed55fa368849fc Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 13 May 2026 16:59:08 +0200 Subject: [PATCH 03/14] refactor: move product image resolution from dereference to validate Image resolution is a pure computation, not an I/O dereference, so it belongs in validate_cluster alongside the other config validation. This aligns with the pattern used by the trino and airflow operators. Co-Authored-By: Claude Opus 4.6 --- .../src/controller/dereference.rs | 17 -------------- .../src/controller/validate.rs | 22 ++++++++++++++----- rust/operator-binary/src/hbase_controller.rs | 15 ++++++------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs index b48f26a3..78615bd9 100644 --- a/rust/operator-binary/src/controller/dereference.rs +++ b/rust/operator-binary/src/controller/dereference.rs @@ -1,5 +1,4 @@ use snafu::{ResultExt, Snafu}; -use stackable_operator::commons::product_image_selection::{self, ResolvedProductImage}; use crate::{ crd::v1alpha1, security::opa::HbaseOpaConfig, zookeeper::ZookeeperConnectionInformation, @@ -7,11 +6,6 @@ use crate::{ #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("failed to resolve product image"))] - ResolveProductImage { - source: product_image_selection::Error, - }, - #[snafu(display("failed to retrieve zookeeper connection information"))] RetrieveZookeeperConnectionInformation { source: crate::zookeeper::Error }, @@ -21,7 +15,6 @@ pub enum Error { /// External references resolved during the dereference step. pub struct DereferencedObjects { - pub resolved_product_image: ResolvedProductImage, pub zookeeper_connection_information: ZookeeperConnectionInformation, pub hbase_opa_config: Option, } @@ -29,16 +22,7 @@ pub struct DereferencedObjects { pub async fn dereference( client: &stackable_operator::client::Client, hbase: &v1alpha1::HbaseCluster, - image_base_name: &str, - image_repository: &str, - pkg_version: &str, ) -> Result { - let resolved_product_image = hbase - .spec - .image - .resolve(image_base_name, image_repository, pkg_version) - .context(ResolveProductImageSnafu)?; - let zookeeper_connection_information = ZookeeperConnectionInformation::retrieve(hbase, client) .await .context(RetrieveZookeeperConnectionInformationSnafu)?; @@ -53,7 +37,6 @@ pub async fn dereference( }; Ok(DereferencedObjects { - resolved_product_image, zookeeper_connection_information, hbase_opa_config, }) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 947f0176..685508b5 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -6,16 +6,20 @@ use std::{ use product_config::{ProductConfigManager, types::PropertyNameKind}; use snafu::{ResultExt, Snafu}; use stackable_operator::{ - commons::product_image_selection::ResolvedProductImage, + commons::product_image_selection::{self, ResolvedProductImage}, product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, role_utils::GenericRoleConfig, }; -use super::dereference::DereferencedObjects; 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 }, @@ -63,13 +67,21 @@ pub struct ValidatedHbaseCluster { pub fn validate_cluster( hbase: &v1alpha1::HbaseCluster, - dereferenced: &DereferencedObjects, + 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( - &dereferenced.resolved_product_image.product_version, + &resolved_product_image.product_version, &transform_all_roles_to_config(hbase, &roles).context(GenerateProductConfigSnafu)?, product_config_manager, false, @@ -117,7 +129,7 @@ pub fn validate_cluster( } Ok(ValidatedHbaseCluster { - image: dereferenced.resolved_product_image.clone(), + image: resolved_product_image, role_groups, role_configs, }) diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index a7528307..fa2caa73 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -298,19 +298,18 @@ pub async fn reconcile_hbase( let client = &ctx.client; - let dereferenced = crate::controller::dereference::dereference( - client, + let dereferenced = crate::controller::dereference::dereference(client, hbase) + .await + .context(DereferenceSnafu)?; + + 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, ) - .await - .context(DereferenceSnafu)?; - - let validated = - crate::controller::validate::validate_cluster(hbase, &dereferenced, &ctx.product_config) - .context(ValidateSnafu)?; + .context(ValidateSnafu)?; let mut cluster_resources = ClusterResources::new( APP_NAME, From 7834d93641fbce8948de0941a34ac6b09f82aeb6 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 13 May 2026 17:25:00 +0200 Subject: [PATCH 04/14] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 765c2230..9bb5ecd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,13 @@ `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 From 2123858bc4edd6f9c6e303cbe4dc03422169ffb6 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 13 May 2026 17:44:25 +0200 Subject: [PATCH 05/14] linting --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bb5ecd5..895e788d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ - 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 From d01c32165b064b62fa89566e9e5727cc48ee046d Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 18 May 2026 12:24:23 +0200 Subject: [PATCH 06/14] use constant for image base name --- rust/operator-binary/src/controller/validate.rs | 5 ++--- rust/operator-binary/src/hbase_controller.rs | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 685508b5..c9163b3c 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -11,7 +11,7 @@ use stackable_operator::{ role_utils::GenericRoleConfig, }; -use crate::crd::{AnyServiceConfig, HbaseRole, v1alpha1}; +use crate::{crd::{AnyServiceConfig, HbaseRole, v1alpha1}, hbase_controller::CONTAINER_IMAGE_BASE_NAME}; #[derive(Snafu, Debug)] pub enum Error { @@ -67,7 +67,6 @@ pub struct ValidatedHbaseCluster { pub fn validate_cluster( hbase: &v1alpha1::HbaseCluster, - image_base_name: &str, image_repository: &str, pkg_version: &str, product_config_manager: &ProductConfigManager, @@ -75,7 +74,7 @@ pub fn validate_cluster( let resolved_product_image = hbase .spec .image - .resolve(image_base_name, image_repository, pkg_version) + .resolve(CONTAINER_IMAGE_BASE_NAME, image_repository, pkg_version) .context(ResolveProductImageSnafu)?; let roles = hbase.build_role_properties().context(RolePropertiesSnafu)?; diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index fa2caa73..e44a43e2 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -103,7 +103,7 @@ const HDFS_DISCOVERY_TMP_DIR: &str = "/stackable/tmp/hdfs"; const HBASE_CONFIG_TMP_DIR: &str = "/stackable/tmp/hbase"; const HBASE_LOG_CONFIG_TMP_DIR: &str = "/stackable/tmp/log_config"; -const CONTAINER_IMAGE_BASE_NAME: &str = "hbase"; +pub const CONTAINER_IMAGE_BASE_NAME: &str = "hbase"; pub struct Ctx { pub client: stackable_operator::client::Client, @@ -304,7 +304,6 @@ pub async fn reconcile_hbase( 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, From 979d647f95c09b4bdb4fbc9e984d298379447416 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 18 May 2026 12:29:12 +0200 Subject: [PATCH 07/14] formatting --- rust/operator-binary/src/controller/validate.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index c9163b3c..0d279731 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -11,7 +11,10 @@ use stackable_operator::{ role_utils::GenericRoleConfig, }; -use crate::{crd::{AnyServiceConfig, HbaseRole, v1alpha1}, hbase_controller::CONTAINER_IMAGE_BASE_NAME}; +use crate::{ + crd::{AnyServiceConfig, HbaseRole, v1alpha1}, + hbase_controller::CONTAINER_IMAGE_BASE_NAME, +}; #[derive(Snafu, Debug)] pub enum Error { From 006ee58431a59348b11fbf3f9e4e5dcd63e502ac Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 18 May 2026 15:27:20 +0200 Subject: [PATCH 08/14] move validated struct to controller, add dereferenced fields to validated cluster struct --- .../src/controller/validate.rs | 23 ++++++++----------- rust/operator-binary/src/hbase_controller.rs | 22 +++++++++++++++--- rust/operator-binary/src/security/opa.rs | 1 + rust/operator-binary/src/zookeeper.rs | 1 + 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 0d279731..5664bfd6 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -6,14 +6,16 @@ use std::{ use product_config::{ProductConfigManager, types::PropertyNameKind}; use snafu::{ResultExt, Snafu}; use stackable_operator::{ - commons::product_image_selection::{self, ResolvedProductImage}, + commons::product_image_selection::{self}, product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, role_utils::GenericRoleConfig, }; use crate::{ crd::{AnyServiceConfig, HbaseRole, v1alpha1}, - hbase_controller::CONTAINER_IMAGE_BASE_NAME, + hbase_controller::{CONTAINER_IMAGE_BASE_NAME, ValidatedCluster}, + security::opa::HbaseOpaConfig, + zookeeper::ZookeeperConnectionInformation, }; #[derive(Snafu, Debug)] @@ -59,21 +61,14 @@ pub struct ValidatedRoleGroupConfig { 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_repository: &str, pkg_version: &str, product_config_manager: &ProductConfigManager, -) -> Result { + zookeeper_connection_information: ZookeeperConnectionInformation, + hbase_opa_config: Option, +) -> Result { let resolved_product_image = hbase .spec .image @@ -130,9 +125,11 @@ pub fn validate_cluster( role_groups.insert(hbase_role, group_configs); } - Ok(ValidatedHbaseCluster { + Ok(ValidatedCluster { image: resolved_product_image, role_groups, role_configs, + zookeeper_connection_information, + hbase_opa_config, }) } diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index e44a43e2..54bd01e0 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -71,6 +71,7 @@ use crate::{ construct_global_jvm_args, construct_hbase_heapsize_env, construct_role_specific_non_heap_jvm_args, }, + controller::validate::{ValidatedRoleConfig, ValidatedRoleGroupConfig}, crd::{ APP_NAME, AnyServiceConfig, Container, HBASE_ENV_SH, HBASE_MASTER_PORT, HBASE_MASTER_UI_PORT, HBASE_REGIONSERVER_PORT, HBASE_REGIONSERVER_UI_PORT, HBASE_SITE_XML, @@ -111,6 +112,19 @@ pub struct Ctx { pub operator_environment: OperatorEnvironmentOptions, } +/// The validated cluster: proves that product-config validation and config merging +/// succeeded for every role and role group before any resources are created. +/// Placed in the controller so that subsequent steps that reference this struct +/// only depend on the controller. +#[derive(Clone, Debug)] +pub struct ValidatedCluster { + pub image: ResolvedProductImage, + pub role_groups: BTreeMap>, + pub role_configs: BTreeMap, + pub zookeeper_connection_information: ZookeeperConnectionInformation, + pub hbase_opa_config: Option, +} + #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { @@ -307,6 +321,8 @@ pub async fn reconcile_hbase( &ctx.operator_environment.image_repository, crate::built_info::PKG_VERSION, &ctx.product_config, + dereferenced.zookeeper_connection_information, + dereferenced.hbase_opa_config, ) .context(ValidateSnafu)?; @@ -354,10 +370,10 @@ pub async fn reconcile_hbase( &client.kubernetes_cluster_info, &rolegroup, &validated_rg_config.product_config_properties, - &dereferenced.zookeeper_connection_information, + &validated.zookeeper_connection_information, &validated_rg_config.merged_config, &validated.image, - dereferenced.hbase_opa_config.as_ref(), + validated.hbase_opa_config.as_ref(), )?; let rg_statefulset = build_rolegroup_statefulset( hbase, @@ -418,7 +434,7 @@ pub async fn reconcile_hbase( let discovery_cm = build_discovery_configmap( hbase, &client.kubernetes_cluster_info, - &dereferenced.zookeeper_connection_information, + &validated.zookeeper_connection_information, &validated.image, ) .context(BuildDiscoveryConfigMapSnafu)?; diff --git a/rust/operator-binary/src/security/opa.rs b/rust/operator-binary/src/security/opa.rs index 500c4b22..760f0b79 100644 --- a/rust/operator-binary/src/security/opa.rs +++ b/rust/operator-binary/src/security/opa.rs @@ -19,6 +19,7 @@ pub enum Error { type Result = std::result::Result; +#[derive(Clone, Debug)] pub struct HbaseOpaConfig { authorization_connection_string: String, dry_run: bool, diff --git a/rust/operator-binary/src/zookeeper.rs b/rust/operator-binary/src/zookeeper.rs index e518f9a1..ce558354 100644 --- a/rust/operator-binary/src/zookeeper.rs +++ b/rust/operator-binary/src/zookeeper.rs @@ -43,6 +43,7 @@ pub enum Error { type Result = std::result::Result; /// Contains the information as exposed by the Zookeeper/Znode discovery CM (should work with both) +#[derive(Clone, Debug)] pub struct ZookeeperConnectionInformation { /// E.g. `simple-zk-server-default-0.simple-zk-server-default.default.svc.cluster.local:2282,simple-zk-server-default-1.simple-zk-server-default.default.svc.cluster.local:2282,simple-zk-server-default-2.simple-zk-server-default.default.svc.cluster.local:2282` hosts: String, From d020ce999f362523320b418c225bcac4b9eaaf35 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 18 May 2026 16:08:48 +0200 Subject: [PATCH 09/14] remove securityContext from assert (might not work on Openshift) --- tests/templates/kuttl/smoke/30-assert.yaml | 78 -- tests/templates/kuttl/smoke/30-assert.yaml.j2 | 892 ++++++++++++++++++ 2 files changed, 892 insertions(+), 78 deletions(-) delete mode 100644 tests/templates/kuttl/smoke/30-assert.yaml create mode 100644 tests/templates/kuttl/smoke/30-assert.yaml.j2 diff --git a/tests/templates/kuttl/smoke/30-assert.yaml b/tests/templates/kuttl/smoke/30-assert.yaml deleted file mode 100644 index a777cf48..00000000 --- a/tests/templates/kuttl/smoke/30-assert.yaml +++ /dev/null @@ -1,78 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -metadata: - name: install-hbase -timeout: 600 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-hbase-master-default - generation: 1 # There should be no unneeded Pod restarts - labels: - restarter.stackable.tech/enabled: "true" -spec: - template: - spec: - terminationGracePeriodSeconds: 1200 -status: - readyReplicas: 2 - replicas: 2 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-hbase-regionserver-default - generation: 1 # There should be no unneeded Pod restarts - labels: - restarter.stackable.tech/enabled: "true" -spec: - template: - spec: - terminationGracePeriodSeconds: 3600 -status: - readyReplicas: 2 - replicas: 2 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-hbase-restserver-default - generation: 1 # There should be no unneeded Pod restarts - labels: - restarter.stackable.tech/enabled: "true" -spec: - template: - spec: - terminationGracePeriodSeconds: 300 -status: - readyReplicas: 2 - replicas: 2 ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: test-hbase-master -status: - expectedPods: 2 - currentHealthy: 2 - disruptionsAllowed: 1 ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: test-hbase-regionserver -status: - expectedPods: 2 - currentHealthy: 2 - disruptionsAllowed: 1 ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: test-hbase-restserver -status: - expectedPods: 2 - currentHealthy: 2 - disruptionsAllowed: 1 diff --git a/tests/templates/kuttl/smoke/30-assert.yaml.j2 b/tests/templates/kuttl/smoke/30-assert.yaml.j2 new file mode 100644 index 00000000..5396ea20 --- /dev/null +++ b/tests/templates/kuttl/smoke/30-assert.yaml.j2 @@ -0,0 +1,892 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: install-hbase +timeout: 600 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-master-default-headless +spec: + clusterIP: None + ports: + - name: master + port: 16000 + protocol: TCP + targetPort: 16000 + - name: ui-http + port: 16010 + protocol: TCP + targetPort: 16010 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + prometheus.io/path: /prometheus + prometheus.io/port: "16010" + prometheus.io/scheme: http + prometheus.io/scrape: "true" + labels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + prometheus.io/scrape: "true" + stackable.tech/vendor: Stackable + name: test-hbase-master-default-metrics +spec: + clusterIP: None + ports: + - name: metrics + port: 16010 + protocol: TCP + targetPort: 16010 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-regionserver-default-headless +spec: + clusterIP: None + ports: + - name: regionserver + port: 16020 + protocol: TCP + targetPort: 16020 + - name: ui-http + port: 16030 + protocol: TCP + targetPort: 16030 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + prometheus.io/path: /prometheus + prometheus.io/port: "16030" + prometheus.io/scheme: http + prometheus.io/scrape: "true" + labels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + prometheus.io/scrape: "true" + stackable.tech/vendor: Stackable + name: test-hbase-regionserver-default-metrics +spec: + clusterIP: None + ports: + - name: metrics + port: 16030 + protocol: TCP + targetPort: 16030 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-restserver-default-headless +spec: + clusterIP: None + ports: + - name: rest-http + port: 8080 + protocol: TCP + targetPort: 8080 + - name: ui-http + port: 8085 + protocol: TCP + targetPort: 8085 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + prometheus.io/path: /prometheus + prometheus.io/port: "8085" + prometheus.io/scheme: http + prometheus.io/scrape: "true" + labels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + prometheus.io/scrape: "true" + stackable.tech/vendor: Stackable + name: test-hbase-restserver-default-metrics +spec: + clusterIP: None + ports: + - name: metrics + port: 8085 + protocol: TCP + targetPort: 8085 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-hbase-master-default + generation: 1 # There should be no unneeded Pod restarts + labels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + restarter.stackable.tech/enabled: "true" + stackable.tech/vendor: Stackable +spec: + podManagementPolicy: Parallel + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + serviceName: test-hbase-master-default-headless + template: + metadata: + labels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + spec: + affinity: + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + topologyKey: kubernetes.io/hostname + weight: 20 + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + topologyKey: kubernetes.io/hostname + weight: 70 + containers: + - args: + - /stackable/hbase/bin/hbase-entrypoint.sh master 16000 master ui-http + command: + - /bin/bash + - -x + - -euo + - pipefail + - -c + env: + - name: HADOOP_CONF_DIR + value: /stackable/conf + - name: HBASE_CONF_DIR + value: /stackable/conf + - name: REGION_MOVER_OPTS + - name: RUN_REGION_MOVER + value: "false" + - name: STACKABLE_LOG_DIR + value: /stackable/log + - name: CONTAINERDEBUG_LOG_DIRECTORY + value: /stackable/log/containerdebug + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: master + timeoutSeconds: 3 + name: hbase + ports: + - containerPort: 16000 + name: master + protocol: TCP + - containerPort: 16010 + name: ui-http + protocol: TCP + readinessProbe: + failureThreshold: 1 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: master + timeoutSeconds: 2 + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: 250m + memory: 1Gi + startupProbe: + failureThreshold: 120 + initialDelaySeconds: 4 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: master + timeoutSeconds: 3 + volumeMounts: + - mountPath: /stackable/tmp/hbase + name: hbase-config + - mountPath: /stackable/tmp/hdfs + name: hdfs-discovery + - mountPath: /stackable/tmp/log_config + name: log-config + - mountPath: /stackable/log + name: log + - mountPath: /stackable/listener + name: listener + enableServiceLinks: false + serviceAccountName: test-hbase-serviceaccount + terminationGracePeriodSeconds: 1200 + volumes: + - configMap: + name: test-hbase-master-default + name: hbase-config + - configMap: + name: test-hdfs + name: hdfs-discovery + - emptyDir: + sizeLimit: 30Mi + name: log + - configMap: + name: test-hbase-master-default + name: log-config + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + listeners.stackable.tech/listener-class: {{ test_scenario['values']['listener-class'] }} + spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: "1" + storageClassName: listeners.stackable.tech + name: listener +status: + readyReplicas: 2 + replicas: 2 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-hbase-regionserver-default + generation: 1 # There should be no unneeded Pod restarts + labels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + restarter.stackable.tech/enabled: "true" + stackable.tech/vendor: Stackable +spec: + podManagementPolicy: Parallel + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + serviceName: test-hbase-regionserver-default-headless + template: + metadata: + labels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + spec: + affinity: + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + topologyKey: kubernetes.io/hostname + weight: 20 + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: datanode + app.kubernetes.io/instance: test-hdfs + app.kubernetes.io/name: hdfs + topologyKey: kubernetes.io/hostname + weight: 50 + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + topologyKey: kubernetes.io/hostname + weight: 70 + containers: + - args: + - /stackable/hbase/bin/hbase-entrypoint.sh regionserver 16020 regionserver + ui-http + command: + - /bin/bash + - -x + - -euo + - pipefail + - -c + env: + - name: HADOOP_CONF_DIR + value: /stackable/conf + - name: HBASE_CONF_DIR + value: /stackable/conf + - name: REGION_MOVER_OPTS + - name: RUN_REGION_MOVER + value: "false" + - name: STACKABLE_LOG_DIR + value: /stackable/log + - name: CONTAINERDEBUG_LOG_DIRECTORY + value: /stackable/log/containerdebug + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: regionserver + timeoutSeconds: 3 + name: hbase + ports: + - containerPort: 16020 + name: regionserver + protocol: TCP + - containerPort: 16030 + name: ui-http + protocol: TCP + readinessProbe: + failureThreshold: 1 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: regionserver + timeoutSeconds: 2 + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: 250m + memory: 1Gi + startupProbe: + failureThreshold: 120 + initialDelaySeconds: 4 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: regionserver + timeoutSeconds: 3 + volumeMounts: + - mountPath: /stackable/tmp/hbase + name: hbase-config + - mountPath: /stackable/tmp/hdfs + name: hdfs-discovery + - mountPath: /stackable/tmp/log_config + name: log-config + - mountPath: /stackable/log + name: log + - mountPath: /stackable/listener + name: listener + enableServiceLinks: false + serviceAccountName: test-hbase-serviceaccount + terminationGracePeriodSeconds: 3600 + volumes: + - configMap: + name: test-hbase-regionserver-default + name: hbase-config + - configMap: + name: test-hdfs + name: hdfs-discovery + - emptyDir: + sizeLimit: 30Mi + name: log + - configMap: + name: test-hbase-regionserver-default + name: log-config + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + listeners.stackable.tech/listener-class: {{ test_scenario['values']['listener-class'] }} + spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: "1" + storageClassName: listeners.stackable.tech + name: listener +status: + readyReplicas: 2 + replicas: 2 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-hbase-restserver-default + generation: 1 # There should be no unneeded Pod restarts + labels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + restarter.stackable.tech/enabled: "true" + stackable.tech/vendor: Stackable +spec: + podManagementPolicy: Parallel + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + serviceName: test-hbase-restserver-default-headless + template: + metadata: + labels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + spec: + affinity: + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + topologyKey: kubernetes.io/hostname + weight: 20 + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/name: hbase + topologyKey: kubernetes.io/hostname + weight: 70 + containers: + - args: + - /stackable/hbase/bin/hbase-entrypoint.sh rest 8080 rest-http ui-http + command: + - /bin/bash + - -x + - -euo + - pipefail + - -c + env: + - name: HADOOP_CONF_DIR + value: /stackable/conf + - name: HBASE_CONF_DIR + value: /stackable/conf + - name: REGION_MOVER_OPTS + - name: RUN_REGION_MOVER + value: "false" + - name: STACKABLE_LOG_DIR + value: /stackable/log + - name: CONTAINERDEBUG_LOG_DIRECTORY + value: /stackable/log/containerdebug + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: rest-http + timeoutSeconds: 3 + name: hbase + ports: + - containerPort: 8080 + name: rest-http + protocol: TCP + - containerPort: 8085 + name: ui-http + protocol: TCP + readinessProbe: + failureThreshold: 1 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: rest-http + timeoutSeconds: 2 + resources: + limits: + cpu: 400m + memory: 1Gi + requests: + cpu: 100m + memory: 1Gi + startupProbe: + failureThreshold: 120 + initialDelaySeconds: 4 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: rest-http + timeoutSeconds: 3 + volumeMounts: + - mountPath: /stackable/tmp/hbase + name: hbase-config + - mountPath: /stackable/tmp/hdfs + name: hdfs-discovery + - mountPath: /stackable/tmp/log_config + name: log-config + - mountPath: /stackable/log + name: log + - mountPath: /stackable/listener + name: listener + enableServiceLinks: false + serviceAccountName: test-hbase-serviceaccount + terminationGracePeriodSeconds: 300 + volumes: + - configMap: + name: test-hbase-restserver-default + name: hbase-config + - configMap: + name: test-hdfs + name: hdfs-discovery + - emptyDir: + sizeLimit: 30Mi + name: log + - configMap: + name: test-hbase-restserver-default + name: log-config + volumeClaimTemplates: + - metadata: + annotations: + listeners.stackable.tech/listener-class: {{ test_scenario['values']['listener-class'] }} + labels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: listener + spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: "1" + storageClassName: listeners.stackable.tech +status: + readyReplicas: 2 + replicas: 2 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: discovery + stackable.tech/vendor: Stackable + name: test-hbase + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-master-default + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-regionserver-default + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-restserver-default + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + name: test-hbase-serviceaccount + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + name: test-hbase-rolebinding + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: hbase-clusterrole +subjects: +- kind: ServiceAccount + name: test-hbase-serviceaccount +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-hbase-master +status: + expectedPods: 2 + currentHealthy: 2 + disruptionsAllowed: 1 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-hbase-regionserver +status: + expectedPods: 2 + currentHealthy: 2 + disruptionsAllowed: 1 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-hbase-restserver +status: + expectedPods: 2 + currentHealthy: 2 + disruptionsAllowed: 1 +--- +apiVersion: listeners.stackable.tech/v1alpha1 +kind: Listener +metadata: + labels: + app.kubernetes.io/component: master + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-master-default-0-listener +spec: + className: {{ test_scenario['values']['listener-class'] }} + ports: + - name: master + port: 16000 + protocol: TCP + - name: ui-http + port: 16010 + protocol: TCP + publishNotReadyAddresses: true +status: + ingressAddresses: + - addressType: Hostname + ports: + master: 16000 + ui-http: 16010 + serviceName: test-hbase-master-default-0-listener +--- +apiVersion: listeners.stackable.tech/v1alpha1 +kind: Listener +metadata: + labels: + app.kubernetes.io/component: regionserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: test-hbase-regionserver-default-0-listener +spec: + className: {{ test_scenario['values']['listener-class'] }} + ports: + - name: regionserver + port: 16020 + protocol: TCP + - name: ui-http + port: 16030 + protocol: TCP + publishNotReadyAddresses: true +status: + ingressAddresses: + - addressType: Hostname + ports: + regionserver: 16020 + ui-http: 16030 + serviceName: test-hbase-regionserver-default-0-listener +--- +apiVersion: listeners.stackable.tech/v1alpha1 +kind: Listener +metadata: + labels: + app.kubernetes.io/component: restserver + app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/name: hbase + app.kubernetes.io/role-group: default + stackable.tech/vendor: Stackable + name: listener-test-hbase-restserver-default-0 +spec: + className: {{ test_scenario['values']['listener-class'] }} + ports: + - name: rest-http + port: 8080 + protocol: TCP + - name: ui-http + port: 8085 + protocol: TCP + publishNotReadyAddresses: true +status: + ingressAddresses: + - addressType: Hostname + ports: + rest-http: 8080 + ui-http: 8085 + serviceName: listener-test-hbase-restserver-default-0 From f3be8ef23da072add6552575079239a13d40402a Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 18 May 2026 16:44:42 +0200 Subject: [PATCH 10/14] refine assert --- tests/templates/kuttl/smoke/30-assert.yaml.j2 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/templates/kuttl/smoke/30-assert.yaml.j2 b/tests/templates/kuttl/smoke/30-assert.yaml.j2 index 5396ea20..1a6b2037 100644 --- a/tests/templates/kuttl/smoke/30-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/30-assert.yaml.j2 @@ -827,10 +827,14 @@ spec: publishNotReadyAddresses: true status: ingressAddresses: +{% if test_scenario['values']['listener-class'] == 'cluster-internal' %} - addressType: Hostname ports: master: 16000 ui-http: 16010 +{% else %} + - addressType: IP +{% endif %} serviceName: test-hbase-master-default-0-listener --- apiVersion: listeners.stackable.tech/v1alpha1 @@ -856,10 +860,14 @@ spec: publishNotReadyAddresses: true status: ingressAddresses: +{% if test_scenario['values']['listener-class'] == 'cluster-internal' %} - addressType: Hostname ports: regionserver: 16020 ui-http: 16030 +{% else %} + - addressType: IP +{% endif %} serviceName: test-hbase-regionserver-default-0-listener --- apiVersion: listeners.stackable.tech/v1alpha1 @@ -885,8 +893,12 @@ spec: publishNotReadyAddresses: true status: ingressAddresses: +{% if test_scenario['values']['listener-class'] == 'cluster-internal' %} - addressType: Hostname ports: rest-http: 8080 ui-http: 8085 +{% else %} + - addressType: IP +{% endif %} serviceName: listener-test-hbase-restserver-default-0 From c1326c2bc80669502aa7708d92955785c34df5ed Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 18 May 2026 16:55:46 +0200 Subject: [PATCH 11/14] replace version with compile-time constant --- rust/operator-binary/src/controller/validate.rs | 7 +++++-- rust/operator-binary/src/hbase_controller.rs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 5664bfd6..552fa50a 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -64,7 +64,6 @@ pub struct ValidatedRoleGroupConfig { pub fn validate_cluster( hbase: &v1alpha1::HbaseCluster, image_repository: &str, - pkg_version: &str, product_config_manager: &ProductConfigManager, zookeeper_connection_information: ZookeeperConnectionInformation, hbase_opa_config: Option, @@ -72,7 +71,11 @@ pub fn validate_cluster( let resolved_product_image = hbase .spec .image - .resolve(CONTAINER_IMAGE_BASE_NAME, image_repository, pkg_version) + .resolve( + CONTAINER_IMAGE_BASE_NAME, + image_repository, + crate::built_info::PKG_VERSION, + ) .context(ResolveProductImageSnafu)?; let roles = hbase.build_role_properties().context(RolePropertiesSnafu)?; diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index 54bd01e0..2e80e281 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -319,7 +319,6 @@ pub async fn reconcile_hbase( let validated = crate::controller::validate::validate_cluster( hbase, &ctx.operator_environment.image_repository, - crate::built_info::PKG_VERSION, &ctx.product_config, dereferenced.zookeeper_connection_information, dereferenced.hbase_opa_config, From 3dc6bd39ce2a93d1c7e8449a5b165f63bc7025e8 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 18 May 2026 17:10:11 +0200 Subject: [PATCH 12/14] use already-resolved hbase role --- rust/operator-binary/src/hbase_controller.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index 2e80e281..41d51936 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -3,7 +3,6 @@ use std::{ collections::{BTreeMap, HashMap}, fmt::Write, - str::FromStr, sync::Arc, }; @@ -366,6 +365,7 @@ pub async fn reconcile_hbase( let rg_configmap = build_rolegroup_config_map( hbase, + hbase_role, &client.kubernetes_cluster_info, &rolegroup, &validated_rg_config.product_config_properties, @@ -465,6 +465,7 @@ pub async fn reconcile_hbase( #[allow(clippy::too_many_arguments)] fn build_rolegroup_config_map( hbase: &v1alpha1::HbaseCluster, + hbase_role: &HbaseRole, cluster_info: &KubernetesClusterInfo, rolegroup: &RoleGroupRef, rolegroup_config: &HashMap>, @@ -478,11 +479,6 @@ fn build_rolegroup_config_map( let mut ssl_server_xml = String::new(); let mut ssl_client_xml = String::new(); - let hbase_role = - HbaseRole::from_str(rolegroup.role.as_ref()).with_context(|_| UnknownHbaseRoleSnafu { - role: rolegroup.role.clone(), - })?; - for (property_name_kind, config) in rolegroup_config { match property_name_kind { PropertyNameKind::File(file_name) if file_name == HBASE_SITE_XML => { @@ -579,7 +575,7 @@ fn build_rolegroup_config_map( } PropertyNameKind::File(file_name) if file_name == HBASE_ENV_SH => { let mut hbase_env_config = - build_hbase_env_sh(hbase, merged_config, &hbase_role, &rolegroup.role_group)?; + build_hbase_env_sh(hbase, merged_config, hbase_role, &rolegroup.role_group)?; // configOverride come last hbase_env_config.extend(config.clone()); From a99b6d5e994fb98b2870af0839209714f218977b Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 19 May 2026 13:06:07 +0200 Subject: [PATCH 13/14] nix update --- Cargo.nix | 18 +++++++++--------- crate-hashes.json | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index ea422ad7..fdd7ec68 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4821,7 +4821,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "k8s_version"; authors = [ @@ -9480,7 +9480,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_certs"; authors = [ @@ -9684,7 +9684,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_operator"; authors = [ @@ -9864,7 +9864,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -9899,7 +9899,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_shared"; authors = [ @@ -9980,7 +9980,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_telemetry"; authors = [ @@ -10090,7 +10090,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_versioned"; authors = [ @@ -10140,7 +10140,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10208,7 +10208,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_webhook"; authors = [ diff --git a/crate-hashes.json b/crate-hashes.json index a6396ca0..86f2b840 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#k8s-version@0.1.3": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-certs@0.4.0": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator-derive@0.3.1": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator@0.111.1": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-shared@0.1.0": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-telemetry@0.6.3": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned-macros@0.10.0": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned@0.10.0": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-webhook@0.9.1": "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#k8s-version@0.1.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-certs@0.4.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator-derive@0.3.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator@0.111.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-shared@0.1.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-telemetry@0.6.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned-macros@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-webhook@0.9.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file From c6ed8685aae7f0d302a8d9a607a1c0d576216da5 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 19 May 2026 15:03:37 +0200 Subject: [PATCH 14/14] pass dereferenced struct rather than each field --- rust/operator-binary/src/controller/validate.rs | 10 ++++------ rust/operator-binary/src/hbase_controller.rs | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 552fa50a..8a14ee1f 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -12,10 +12,9 @@ use stackable_operator::{ }; use crate::{ + controller::dereference::DereferencedObjects, crd::{AnyServiceConfig, HbaseRole, v1alpha1}, hbase_controller::{CONTAINER_IMAGE_BASE_NAME, ValidatedCluster}, - security::opa::HbaseOpaConfig, - zookeeper::ZookeeperConnectionInformation, }; #[derive(Snafu, Debug)] @@ -65,8 +64,7 @@ pub fn validate_cluster( hbase: &v1alpha1::HbaseCluster, image_repository: &str, product_config_manager: &ProductConfigManager, - zookeeper_connection_information: ZookeeperConnectionInformation, - hbase_opa_config: Option, + dereferenced_objects: DereferencedObjects, ) -> Result { let resolved_product_image = hbase .spec @@ -132,7 +130,7 @@ pub fn validate_cluster( image: resolved_product_image, role_groups, role_configs, - zookeeper_connection_information, - hbase_opa_config, + zookeeper_connection_information: dereferenced_objects.zookeeper_connection_information, + hbase_opa_config: dereferenced_objects.hbase_opa_config, }) } diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index 41d51936..ed74ed6c 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -311,7 +311,7 @@ pub async fn reconcile_hbase( let client = &ctx.client; - let dereferenced = crate::controller::dereference::dereference(client, hbase) + let dereferenced_objects = crate::controller::dereference::dereference(client, hbase) .await .context(DereferenceSnafu)?; @@ -319,8 +319,7 @@ pub async fn reconcile_hbase( hbase, &ctx.operator_environment.image_repository, &ctx.product_config, - dereferenced.zookeeper_connection_information, - dereferenced.hbase_opa_config, + dereferenced_objects, ) .context(ValidateSnafu)?;