From 54ff1ae925d6dc0c3638f1baee3d94ef7827f939 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 18:01:41 +0200 Subject: [PATCH 1/9] refactor(security): add sync ZookeeperSecurity::new constructor --- rust/operator-binary/src/crd/security.rs | 27 +++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/crd/security.rs index c7c05930..da811c3d 100644 --- a/rust/operator-binary/src/crd/security.rs +++ b/rust/operator-binary/src/crd/security.rs @@ -102,13 +102,24 @@ impl ZookeeperSecurity { client: &Client, zk: &v1alpha1::ZookeeperCluster, ) -> Result { - Ok(ZookeeperSecurity { - resolved_authentication_classes: authentication::resolve_authentication_classes( - client, - &zk.spec.cluster_config.authentication, - ) - .await - .context(InvalidAuthenticationClassConfigurationSnafu)?, + let resolved_authentication_classes = authentication::resolve_authentication_classes( + client, + &zk.spec.cluster_config.authentication, + ) + .await + .context(InvalidAuthenticationClassConfigurationSnafu)?; + Ok(Self::new(zk, resolved_authentication_classes)) + } + + /// Build a `ZookeeperSecurity` from a [`v1alpha1::ZookeeperCluster`] and already-resolved + /// [`ResolvedAuthenticationClasses`]. Synchronous; intended to be called from the validate + /// step of the controllers. + pub fn new( + zk: &v1alpha1::ZookeeperCluster, + resolved_authentication_classes: ResolvedAuthenticationClasses, + ) -> Self { + ZookeeperSecurity { + resolved_authentication_classes, server_secret_class: zk .spec .cluster_config @@ -122,7 +133,7 @@ impl ZookeeperSecurity { .as_ref() .map(|tls| tls.quorum_secret_class.clone()) .unwrap_or_else(tls::quorum_tls_default), - }) + } } /// Check if TLS encryption is enabled. This could be due to: From 122cbdf9c01585282a87c1cfb64fe20c90e127b1 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 18:09:57 +0200 Subject: [PATCH 2/9] refactor(zk_controller): extract dereference and validate steps --- rust/operator-binary/src/zk_controller.rs | 106 ++++++--------- .../src/zk_controller/dereference.rs | 44 +++++++ .../src/zk_controller/validate.rs | 121 ++++++++++++++++++ 3 files changed, 203 insertions(+), 68 deletions(-) create mode 100644 rust/operator-binary/src/zk_controller/dereference.rs create mode 100644 rust/operator-binary/src/zk_controller/validate.rs diff --git a/rust/operator-binary/src/zk_controller.rs b/rust/operator-binary/src/zk_controller.rs index a706c600..37c1496b 100644 --- a/rust/operator-binary/src/zk_controller.rs +++ b/rust/operator-binary/src/zk_controller.rs @@ -31,7 +31,7 @@ use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, commons::{ - product_image_selection::{self, ResolvedProductImage}, + product_image_selection::ResolvedProductImage, rbac::build_rbac_resources, }, constants::RESTART_CONTROLLER_ENABLED_LABEL, @@ -55,7 +55,6 @@ use stackable_operator::{ }, kvp::{LabelError, Labels}, logging::controller::ReconcilerError, - product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, product_logging::{ self, framework::{ @@ -81,7 +80,7 @@ use crate::{ command::create_init_container_command_args, config::jvm::{construct_non_heap_jvm_args, construct_zk_server_heap_env}, crd::{ - CONTAINER_IMAGE_BASE_NAME, JMX_METRICS_PORT_NAME, JVM_SECURITY_PROPERTIES_FILE, + JMX_METRICS_PORT_NAME, JVM_SECURITY_PROPERTIES_FILE, MAX_PREPARE_LOG_FILE_SIZE, MAX_ZK_LOG_FILES_SIZE, METRICS_PROVIDER_HTTP_PORT_NAME, STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, STACKABLE_RW_CONFIG_DIR, ZOOKEEPER_ELECTION_PORT, ZOOKEEPER_ELECTION_PORT_NAME, @@ -101,6 +100,9 @@ use crate::{ utils::build_recommended_labels, }; +mod dereference; +mod validate; + pub const ZK_CONTROLLER_NAME: &str = "zookeepercluster"; pub const ZK_FULL_CONTROLLER_NAME: &str = concatcp!(ZK_CONTROLLER_NAME, '.', OPERATOR_NAME); pub const LISTENER_VOLUME_NAME: &str = "listener"; @@ -126,12 +128,15 @@ pub enum Error { source: error_boundary::InvalidObject, }, + #[snafu(display("failed to dereference resources"))] + Dereference { source: dereference::Error }, + + #[snafu(display("failed to validate cluster"))] + ValidateCluster { source: validate::Error }, + #[snafu(display("crd validation failure"))] CrdValidationFailure { source: crate::crd::Error }, - #[snafu(display("object defines no server role"))] - NoServerRole, - #[snafu(display("could not parse role [{role}]"))] RoleParseFailure { source: strum::ParseError, @@ -165,16 +170,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 serialize [{ZOOKEEPER_PROPERTIES_FILE}] for {}", rolegroup))] SerializeZooCfg { source: PropertiesWriterError, @@ -228,9 +223,6 @@ pub enum Error { cm_name: String, }, - #[snafu(display("failed to initialize security context"))] - FailedToInitializeSecurityContext { source: crate::crd::security::Error }, - #[snafu(display("failed to resolve and merge config for role and role group"))] FailedToResolveConfig { source: crate::crd::Error }, @@ -289,11 +281,6 @@ pub enum Error { source: stackable_operator::builder::pod::volume::ListenerOperatorVolumeSourceBuilderError, }, - #[snafu(display("failed to resolve product image"))] - ResolveProductImage { - source: product_image_selection::Error, - }, - #[snafu(display("failed to build service"))] BuildService { source: service::Error }, @@ -310,16 +297,15 @@ impl ReconcilerError for Error { match self { Error::MissingSecretLifetime => None, Error::InvalidZookeeperCluster { .. } => None, + Error::Dereference { .. } => None, + Error::ValidateCluster { .. } => None, Error::CrdValidationFailure { .. } => None, - Error::NoServerRole => None, Error::RoleParseFailure { .. } => None, Error::InternalOperatorFailure { .. } => None, Error::ApplyRoleGroupService { .. } => None, Error::BuildRoleGroupConfig { .. } => None, Error::ApplyRoleGroupConfig { .. } => None, Error::ApplyRoleGroupStatefulSet { .. } => None, - Error::GenerateProductConfig { .. } => None, - Error::InvalidProductConfig { .. } => None, Error::SerializeZooCfg { .. } => None, Error::ObjectMissingMetadataForOwnerRef { .. } => None, Error::BuildDiscoveryConfig { .. } => None, @@ -331,7 +317,6 @@ impl ReconcilerError for Error { Error::DeleteOrphans { .. } => None, Error::VectorAggregatorConfigMapMissing => None, Error::InvalidLoggingConfig { .. } => None, - Error::FailedToInitializeSecurityContext { .. } => None, Error::FailedToResolveConfig { .. } => None, Error::FailedToCreatePdb { .. } => None, Error::GracefulShutdown { .. } => None, @@ -346,7 +331,6 @@ impl ReconcilerError for Error { Error::ApplyGroupListener { .. } => None, Error::BuildListenerPersistentVolume { .. } => None, Error::ListenerConfiguration { .. } => None, - Error::ResolveProductImage { .. } => None, Error::BuildService { .. } => None, Error::RetrieveMetricsPortFromConfig { .. } => None, } @@ -364,15 +348,23 @@ pub async fn reconcile_zk( .context(InvalidZookeeperClusterSnafu)?; let client = &ctx.client; - let resolved_product_image = zk - .spec - .image - .resolve( - CONTAINER_IMAGE_BASE_NAME, - &ctx.operator_environment.image_repository, - crate::built_info::PKG_VERSION, - ) - .context(ResolveProductImageSnafu)?; + // dereference (client required) + let dereferenced_objects = dereference::dereference(client, zk) + .await + .context(DereferenceSnafu)?; + + // validate (no client required) + let validate::ValidatedInputs { + resolved_product_image, + zookeeper_security, + validated_role_config, + } = validate::validate( + zk, + &dereferenced_objects, + &ctx.operator_environment, + &ctx.product_config, + ) + .context(ValidateClusterSnafu)?; let mut cluster_resources = ClusterResources::new( APP_NAME, @@ -384,39 +376,11 @@ pub async fn reconcile_zk( ) .context(CreateClusterResourcesSnafu)?; - let validated_config = validate_all_roles_and_groups_config( - &resolved_product_image.product_version, - &transform_all_roles_to_config( - zk, - &[( - ZookeeperRole::Server.to_string(), - ( - vec![ - PropertyNameKind::Env, - PropertyNameKind::File(ZOOKEEPER_PROPERTIES_FILE.to_string()), - PropertyNameKind::File(JVM_SECURITY_PROPERTIES_FILE.to_string()), - ], - zk.spec.servers.clone().context(NoServerRoleSnafu)?, - ), - )] - .into(), - ) - .context(GenerateProductConfigSnafu)?, - &ctx.product_config, - false, - false, - ) - .context(InvalidProductConfigSnafu)?; - - let role_server_config = validated_config + let role_server_config = validated_role_config .get(&ZookeeperRole::Server.to_string()) .map(Cow::Borrowed) .unwrap_or_default(); - let zookeeper_security = ZookeeperSecurity::new_from_zookeeper_cluster(client, zk) - .await - .context(FailedToInitializeSecurityContextSnafu)?; - let (rbac_sa, rbac_rolebinding) = build_rbac_resources( zk, APP_NAME, @@ -1072,9 +1036,15 @@ pub fn error_policy( #[cfg(test)] mod tests { - use stackable_operator::commons::networking::DomainName; + use stackable_operator::{ + commons::networking::DomainName, + product_config_utils::{ + transform_all_roles_to_config, validate_all_roles_and_groups_config, + }, + }; use super::*; + use crate::crd::CONTAINER_IMAGE_BASE_NAME; #[test] fn test_default_config() { diff --git a/rust/operator-binary/src/zk_controller/dereference.rs b/rust/operator-binary/src/zk_controller/dereference.rs new file mode 100644 index 00000000..f7cbd17b --- /dev/null +++ b/rust/operator-binary/src/zk_controller/dereference.rs @@ -0,0 +1,44 @@ +//! The dereference step in the ZookeeperCluster controller. +//! +//! Fetches all Kubernetes objects referenced by the [`v1alpha1::ZookeeperCluster`] spec and +//! returns them in [`DereferencedObjects`]. Synchronous validation of the fetched objects +//! (image resolution, product-config validation, security struct assembly) happens in the +//! validate step. + +use snafu::{ResultExt, Snafu}; +use stackable_operator::client::Client; + +use crate::crd::{ + authentication::{self, ResolvedAuthenticationClasses}, + v1alpha1, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to resolve authentication classes"))] + ResolveAuthenticationClasses { source: authentication::Error }, +} + +type Result = std::result::Result; + +/// Kubernetes objects referenced from the [`v1alpha1::ZookeeperCluster`] spec, already fetched. +pub struct DereferencedObjects { + pub resolved_authentication_classes: ResolvedAuthenticationClasses, +} + +/// Fetches all Kubernetes objects referenced from the [`v1alpha1::ZookeeperCluster`] spec. +pub async fn dereference( + client: &Client, + zk: &v1alpha1::ZookeeperCluster, +) -> Result { + let resolved_authentication_classes = authentication::resolve_authentication_classes( + client, + &zk.spec.cluster_config.authentication, + ) + .await + .context(ResolveAuthenticationClassesSnafu)?; + + Ok(DereferencedObjects { + resolved_authentication_classes, + }) +} diff --git a/rust/operator-binary/src/zk_controller/validate.rs b/rust/operator-binary/src/zk_controller/validate.rs new file mode 100644 index 00000000..be9b9866 --- /dev/null +++ b/rust/operator-binary/src/zk_controller/validate.rs @@ -0,0 +1,121 @@ +//! The validate step in the ZookeeperCluster controller. +//! +//! Synchronously validates inputs that don't require a Kubernetes client. Produces +//! [`ValidatedInputs`], consumed by the rest of `reconcile_zk`. + +use product_config::{ProductConfigManager, types::PropertyNameKind}; +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_operator::{ + cli::OperatorEnvironmentOptions, + commons::product_image_selection::{self, ResolvedProductImage}, + product_config_utils::{ + ValidatedRoleConfigByPropertyKind, transform_all_roles_to_config, + validate_all_roles_and_groups_config, + }, +}; + +use crate::{ + crd::{ + CONTAINER_IMAGE_BASE_NAME, JVM_SECURITY_PROPERTIES_FILE, ZOOKEEPER_PROPERTIES_FILE, + ZookeeperRole, + security::ZookeeperSecurity, + v1alpha1, + }, + zk_controller::dereference::DereferencedObjects, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to resolve product image"))] + ResolveProductImage { + source: product_image_selection::Error, + }, + + #[snafu(display("object defines no server role"))] + NoServerRole, + + #[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, + }, +} + +type Result = std::result::Result; + +/// Synchronous inputs the rest of `reconcile_zk` needs after dereferencing. +pub struct ValidatedInputs { + pub resolved_product_image: ResolvedProductImage, + pub zookeeper_security: ZookeeperSecurity, + pub validated_role_config: ValidatedRoleConfigByPropertyKind, +} + +/// Validates the cluster spec and the dereferenced inputs. +pub fn validate( + zk: &v1alpha1::ZookeeperCluster, + dereferenced_objects: &DereferencedObjects, + operator_environment: &OperatorEnvironmentOptions, + product_config: &ProductConfigManager, +) -> Result { + let resolved_product_image = zk + .spec + .image + .resolve( + CONTAINER_IMAGE_BASE_NAME, + &operator_environment.image_repository, + crate::built_info::PKG_VERSION, + ) + .context(ResolveProductImageSnafu)?; + + let zookeeper_security = ZookeeperSecurity::new( + zk, + dereferenced_objects.resolved_authentication_classes.clone(), + ); + + let validated_role_config = + validated_product_config(zk, &resolved_product_image.product_version, product_config)?; + + Ok(ValidatedInputs { + resolved_product_image, + zookeeper_security, + validated_role_config, + }) +} + +fn validated_product_config( + zk: &v1alpha1::ZookeeperCluster, + product_version: &str, + product_config: &ProductConfigManager, +) -> Result { + let server_role = zk.spec.servers.clone().context(NoServerRoleSnafu)?; + + let role_config = transform_all_roles_to_config( + zk, + &[( + ZookeeperRole::Server.to_string(), + ( + vec![ + PropertyNameKind::Env, + PropertyNameKind::File(ZOOKEEPER_PROPERTIES_FILE.to_string()), + PropertyNameKind::File(JVM_SECURITY_PROPERTIES_FILE.to_string()), + ], + server_role, + ), + )] + .into(), + ) + .context(GenerateProductConfigSnafu)?; + + validate_all_roles_and_groups_config( + product_version, + &role_config, + product_config, + false, + false, + ) + .context(InvalidProductConfigSnafu) +} From 2710992d414443e656b6cd027f9fd7f03356d647 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 18:22:22 +0200 Subject: [PATCH 3/9] refactor(znode_controller): extract dereference and validate steps --- rust/operator-binary/src/znode_controller.rs | 152 ++++++------------ .../src/znode_controller/dereference.rs | 96 +++++++++++ .../src/znode_controller/validate.rs | 59 +++++++ 3 files changed, 208 insertions(+), 99 deletions(-) create mode 100644 rust/operator-binary/src/znode_controller/dereference.rs create mode 100644 rust/operator-binary/src/znode_controller/validate.rs diff --git a/rust/operator-binary/src/znode_controller.rs b/rust/operator-binary/src/znode_controller.rs index a1d79c89..669e9f7d 100644 --- a/rust/operator-binary/src/znode_controller.rs +++ b/rust/operator-binary/src/znode_controller.rs @@ -8,11 +8,11 @@ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::product_image_selection::{self, ResolvedProductImage}, + commons::product_image_selection::ResolvedProductImage, crd::listener, k8s_openapi::api::core::v1::ConfigMap, kube::{ - self, Resource, + Resource, api::ObjectMeta, core::{DeserializeGuard, DynamicObject, error_boundary}, runtime::{controller, finalizer, reflector::ObjectRef}, @@ -26,11 +26,14 @@ use tracing::{debug, info}; use crate::{ APP_NAME, OPERATOR_NAME, - crd::{CONTAINER_IMAGE_BASE_NAME, ZookeeperRole, security::ZookeeperSecurity, v1alpha1}, + crd::{ZookeeperRole, security::ZookeeperSecurity, v1alpha1}, discovery::{self, build_discovery_configmap}, listener::role_listener_name, }; +mod dereference; +mod validate; + pub const ZNODE_CONTROLLER_NAME: &str = "znode"; pub const ZNODE_FULL_CONTROLLER_NAME: &str = concatcp!(ZNODE_CONTROLLER_NAME, '.', OPERATOR_NAME); @@ -48,25 +51,17 @@ pub enum Error { source: error_boundary::InvalidObject, }, + #[snafu(display("failed to dereference resources"))] + Dereference { source: dereference::Error }, + + #[snafu(display("failed to validate cluster"))] + ValidateCluster { source: validate::Error }, + #[snafu(display( "object is missing metadata that should be created by the Kubernetes cluster", ))] ObjectMissingMetadata, - #[snafu(display("object does not refer to ZookeeperCluster"))] - InvalidZkReference, - - #[snafu(display("could not find {zk:?}"))] - FindZk { - source: stackable_operator::client::Error, - zk: ObjectRef, - }, - - ZkDoesNotExist { - source: stackable_operator::client::Error, - zk: ObjectRef, - }, - #[snafu(display("could not find server role service name for {zk:?}"))] NoZkSvcName { zk: ObjectRef, @@ -124,19 +119,11 @@ pub enum Error { #[snafu(display("object has no namespace"))] ObjectHasNoNamespace, - #[snafu(display("failed to initialize security context"))] - FailedToInitializeSecurityContext { source: crate::crd::security::Error }, - #[snafu(display("Znode {znode:?} missing expected keys (name and/or namespace)"))] ZnodeMissingExpectedKeys { source: stackable_operator::cluster_resources::Error, znode: ObjectRef, }, - - #[snafu(display("failed to resolve product image"))] - ResolveProductImage { - source: product_image_selection::Error, - }, } type Result = std::result::Result; @@ -169,10 +156,9 @@ impl ReconcilerError for Error { fn secondary_object(&self) -> Option> { match self { Error::InvalidZookeeperZnode { .. } => None, + Error::Dereference { .. } => None, + Error::ValidateCluster { .. } => None, Error::ObjectMissingMetadata => None, - Error::InvalidZkReference => None, - Error::FindZk { zk, .. } => Some(zk.clone().erase()), - Error::ZkDoesNotExist { zk, .. } => Some(zk.clone().erase()), Error::NoZkSvcName { zk } => Some(zk.clone().erase()), Error::FindZkSvc { zk, .. } => Some(zk.clone().erase()), Error::NoZkFqdn { zk } => Some(zk.clone().erase()), @@ -184,9 +170,7 @@ impl ReconcilerError for Error { Error::Finalizer { .. } => None, Error::DeleteOrphans { .. } => None, Error::ObjectHasNoNamespace => None, - Error::FailedToInitializeSecurityContext { .. } => None, Error::ZnodeMissingExpectedKeys { .. } => None, - Error::ResolveProductImage { .. } => None, } } } @@ -213,7 +197,8 @@ pub async fn reconcile_znode( }; let client = &ctx.client; - let zk = find_zk_of_znode(client, znode).await; + // dereference (client required) — replaces find_zk_of_znode + let dereferenced_objects = dereference::dereference(client, znode).await; let mut default_status_updates: Option = None; // Store the znode path in the status rather than the object itself, to ensure that only K8s administrators can override it let znode_path = match znode.status.as_ref().and_then(|s| s.znode_path.as_deref()) { @@ -251,21 +236,37 @@ pub async fn reconcile_znode( |ev| async { match ev { finalizer::Event::Apply(znode) => { - let zk = zk?; - let resolved_product_image = zk - .spec - .image - .resolve( - CONTAINER_IMAGE_BASE_NAME, - &ctx.operator_environment.image_repository, - crate::built_info::PKG_VERSION, - ) - .context(ResolveProductImageSnafu)?; - reconcile_apply(client, &znode, Ok(zk), &znode_path, &resolved_product_image) - .await + let dereferenced = dereferenced_objects.context(DereferenceSnafu)?; + let validate::ValidatedInputs { + resolved_product_image, + zookeeper_security, + } = validate::validate(&znode, &dereferenced, &ctx.operator_environment) + .context(ValidateClusterSnafu)?; + reconcile_apply( + client, + &znode, + dereferenced.zk, + &zookeeper_security, + &znode_path, + &resolved_product_image, + ) + .await } - finalizer::Event::Cleanup(_znode) => { - reconcile_cleanup(client, zk, &znode_path).await + finalizer::Event::Cleanup(znode) => { + let dereferenced = match dereferenced_objects { + Ok(d) => d, + Err(dereference::Error::ZkDoesNotExist { zk, .. }) => { + tracing::info!(%zk, "Tried to clean up ZookeeperZnode bound to a ZookeeperCluster that does not exist, assuming it is already gone"); + return Ok(controller::Action::await_change()); + } + Err(e) => return Err(e).context(DereferenceSnafu), + }; + let validate::ValidatedInputs { + zookeeper_security, .. + } = validate::validate(&znode, &dereferenced, &ctx.operator_environment) + .context(ValidateClusterSnafu)?; + reconcile_cleanup(client, dereferenced.zk, &zookeeper_security, &znode_path) + .await } } }, @@ -277,16 +278,11 @@ pub async fn reconcile_znode( async fn reconcile_apply( client: &stackable_operator::client::Client, znode: &v1alpha1::ZookeeperZnode, - zk: Result, + zk: v1alpha1::ZookeeperCluster, + zookeeper_security: &ZookeeperSecurity, znode_path: &str, resolved_product_image: &ResolvedProductImage, ) -> Result { - let zk = zk?; - - let zookeeper_security = ZookeeperSecurity::new_from_zookeeper_cluster(client, &zk) - .await - .context(FailedToInitializeSecurityContextSnafu)?; - let mut cluster_resources = ClusterResources::new( APP_NAME, OPERATOR_NAME, @@ -298,7 +294,7 @@ async fn reconcile_apply( .context(ZnodeMissingExpectedKeysSnafu { znode })?; znode_mgmt::ensure_znode_exists( - &zk_mgmt_addr(&zk, &zookeeper_security, &client.kubernetes_cluster_info)?, + &zk_mgmt_addr(&zk, zookeeper_security, &client.kubernetes_cluster_info)?, znode_path, ) .await @@ -327,7 +323,7 @@ async fn reconcile_apply( listener, Some(znode_path), resolved_product_image, - &zookeeper_security, + zookeeper_security, ) .context(BuildDiscoveryConfigMapSnafu)?; @@ -346,24 +342,13 @@ async fn reconcile_apply( async fn reconcile_cleanup( client: &stackable_operator::client::Client, - zk: Result, + zk: v1alpha1::ZookeeperCluster, + zookeeper_security: &ZookeeperSecurity, znode_path: &str, ) -> Result { - let zk = match zk { - Err(Error::ZkDoesNotExist { zk, .. }) => { - tracing::info!(%zk, "Tried to clean up ZookeeperZnode bound to a ZookeeperCluster that does not exist, assuming it is already gone"); - return Ok(controller::Action::await_change()); - } - res => res?, - }; - - let zookeeper_security = ZookeeperSecurity::new_from_zookeeper_cluster(client, &zk) - .await - .context(FailedToInitializeSecurityContextSnafu)?; - // Clean up znode from the ZooKeeper cluster before letting Kubernetes delete the object znode_mgmt::ensure_znode_missing( - &zk_mgmt_addr(&zk, &zookeeper_security, &client.kubernetes_cluster_info)?, + &zk_mgmt_addr(&zk, zookeeper_security, &client.kubernetes_cluster_info)?, znode_path, ) .await @@ -403,37 +388,6 @@ fn zk_mgmt_addr( )) } -async fn find_zk_of_znode( - client: &stackable_operator::client::Client, - znode: &v1alpha1::ZookeeperZnode, -) -> Result { - let zk_ref = &znode.spec.cluster_ref; - if let (Some(zk_name), Some(zk_ns)) = ( - zk_ref.name.as_deref(), - zk_ref.namespace_relative_from(znode), - ) { - match client - .get::(zk_name, zk_ns) - .await - { - Ok(zk) => Ok(zk), - Err(err) => match &err { - stackable_operator::client::Error::GetResource { - source: kube::Error::Api(s), - .. - } if s.is_not_found() => Err(err).with_context(|_| ZkDoesNotExistSnafu { - zk: ObjectRef::new(zk_name).within(zk_ns), - }), - _ => Err(err).with_context(|_| FindZkSnafu { - zk: ObjectRef::new(zk_name).within(zk_ns), - }), - }, - } - } else { - InvalidZkReferenceSnafu.fail() - } -} - pub fn error_policy( _obj: Arc>, _error: &Error, diff --git a/rust/operator-binary/src/znode_controller/dereference.rs b/rust/operator-binary/src/znode_controller/dereference.rs new file mode 100644 index 00000000..1e79bfd4 --- /dev/null +++ b/rust/operator-binary/src/znode_controller/dereference.rs @@ -0,0 +1,96 @@ +//! The dereference step in the ZookeeperZnode controller. +//! +//! Fetches the parent [`v1alpha1::ZookeeperCluster`] referenced by the znode's +//! `spec.clusterRef`, plus the [`ResolvedAuthenticationClasses`] of that cluster. Both Apply +//! and Cleanup paths in `reconcile_znode` share this output. + +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + client::Client, + kube::{self, runtime::reflector::ObjectRef}, +}; + +use crate::crd::{ + authentication::{self, ResolvedAuthenticationClasses}, + v1alpha1, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("object does not refer to ZookeeperCluster"))] + InvalidZkReference, + + #[snafu(display("could not find {zk:?}"))] + FindZk { + source: stackable_operator::client::Error, + zk: ObjectRef, + }, + + #[snafu(display("could not find {zk:?}"))] + ZkDoesNotExist { + source: stackable_operator::client::Error, + zk: ObjectRef, + }, + + #[snafu(display("failed to resolve authentication classes"))] + ResolveAuthenticationClasses { source: authentication::Error }, +} + +type Result = std::result::Result; + +/// Kubernetes objects referenced from the [`v1alpha1::ZookeeperZnode`] spec, already fetched. +pub struct DereferencedObjects { + pub zk: v1alpha1::ZookeeperCluster, + pub resolved_authentication_classes: ResolvedAuthenticationClasses, +} + +/// Fetches all Kubernetes objects referenced from the [`v1alpha1::ZookeeperZnode`] spec. +pub async fn dereference( + client: &Client, + znode: &v1alpha1::ZookeeperZnode, +) -> Result { + let zk = find_zk_of_znode(client, znode).await?; + + let resolved_authentication_classes = authentication::resolve_authentication_classes( + client, + &zk.spec.cluster_config.authentication, + ) + .await + .context(ResolveAuthenticationClassesSnafu)?; + + Ok(DereferencedObjects { + zk, + resolved_authentication_classes, + }) +} + +async fn find_zk_of_znode( + client: &Client, + znode: &v1alpha1::ZookeeperZnode, +) -> Result { + let zk_ref = &znode.spec.cluster_ref; + let (Some(zk_name), Some(zk_ns)) = ( + zk_ref.name.as_deref(), + zk_ref.namespace_relative_from(znode), + ) else { + return InvalidZkReferenceSnafu.fail(); + }; + + match client + .get::(zk_name, zk_ns) + .await + { + Ok(zk) => Ok(zk), + Err(err) => match &err { + stackable_operator::client::Error::GetResource { + source: kube::Error::Api(s), + .. + } if s.is_not_found() => Err(err).with_context(|_| ZkDoesNotExistSnafu { + zk: ObjectRef::new(zk_name).within(zk_ns), + }), + _ => Err(err).with_context(|_| FindZkSnafu { + zk: ObjectRef::new(zk_name).within(zk_ns), + }), + }, + } +} diff --git a/rust/operator-binary/src/znode_controller/validate.rs b/rust/operator-binary/src/znode_controller/validate.rs new file mode 100644 index 00000000..27ec2e90 --- /dev/null +++ b/rust/operator-binary/src/znode_controller/validate.rs @@ -0,0 +1,59 @@ +//! The validate step in the ZookeeperZnode controller. +//! +//! Synchronously validates inputs that don't require a Kubernetes client. Produces +//! [`ValidatedInputs`], consumed by the rest of `reconcile_znode`. + +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + cli::OperatorEnvironmentOptions, + commons::product_image_selection::{self, ResolvedProductImage}, +}; + +use crate::{ + crd::{CONTAINER_IMAGE_BASE_NAME, security::ZookeeperSecurity, v1alpha1}, + znode_controller::dereference::DereferencedObjects, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to resolve product image"))] + ResolveProductImage { + source: product_image_selection::Error, + }, +} + +type Result = std::result::Result; + +/// Synchronous inputs the rest of `reconcile_znode` needs after dereferencing. +pub struct ValidatedInputs { + pub resolved_product_image: ResolvedProductImage, + pub zookeeper_security: ZookeeperSecurity, +} + +/// Validates the dereferenced inputs. +pub fn validate( + _znode: &v1alpha1::ZookeeperZnode, + dereferenced_objects: &DereferencedObjects, + operator_environment: &OperatorEnvironmentOptions, +) -> Result { + let resolved_product_image = dereferenced_objects + .zk + .spec + .image + .resolve( + CONTAINER_IMAGE_BASE_NAME, + &operator_environment.image_repository, + crate::built_info::PKG_VERSION, + ) + .context(ResolveProductImageSnafu)?; + + let zookeeper_security = ZookeeperSecurity::new( + &dereferenced_objects.zk, + dereferenced_objects.resolved_authentication_classes.clone(), + ); + + Ok(ValidatedInputs { + resolved_product_image, + zookeeper_security, + }) +} From e89256c48861f90442a8ccbed57383f8393010af Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 18:31:45 +0200 Subject: [PATCH 4/9] refactor(security): drop async ZookeeperSecurity::new_from_zookeeper_cluster --- rust/operator-binary/src/crd/security.rs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/crd/security.rs index da811c3d..2f7c43b4 100644 --- a/rust/operator-binary/src/crd/security.rs +++ b/rust/operator-binary/src/crd/security.rs @@ -19,7 +19,6 @@ use stackable_operator::{ }, }, }, - client::Client, commons::secret_class::SecretClassVolumeProvisionParts, crd::authentication::core, k8s_openapi::api::core::v1::Volume, @@ -27,10 +26,7 @@ use stackable_operator::{ }; use crate::{ - crd::{ - authentication::{self, ResolvedAuthenticationClasses}, - tls, v1alpha1, - }, + crd::{authentication::ResolvedAuthenticationClasses, tls, v1alpha1}, zk_controller::LISTENER_VOLUME_NAME, }; @@ -38,9 +34,6 @@ type Result = std::result::Result; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("failed to process authentication class"))] - InvalidAuthenticationClassConfiguration { source: authentication::Error }, - #[snafu(display("failed to build TLS volume for {volume_name:?}"))] BuildTlsVolume { source: SecretOperatorVolumeSourceBuilderError, @@ -96,21 +89,6 @@ impl ZookeeperSecurity { pub const STORE_PASSWORD_ENV: &'static str = "STORE_PASSWORD"; pub const SYSTEM_TRUST_STORE_DIR: &'static str = "/etc/pki/java/cacerts"; - /// Create a `ZookeeperSecurity` struct from the Zookeeper custom resource and resolve - /// all provided `AuthenticationClass` references. - pub async fn new_from_zookeeper_cluster( - client: &Client, - zk: &v1alpha1::ZookeeperCluster, - ) -> Result { - let resolved_authentication_classes = authentication::resolve_authentication_classes( - client, - &zk.spec.cluster_config.authentication, - ) - .await - .context(InvalidAuthenticationClassConfigurationSnafu)?; - Ok(Self::new(zk, resolved_authentication_classes)) - } - /// Build a `ZookeeperSecurity` from a [`v1alpha1::ZookeeperCluster`] and already-resolved /// [`ResolvedAuthenticationClasses`]. Synchronous; intended to be called from the validate /// step of the controllers. From 365bc02fb10e962e22032decb72c799ad2e14806 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 18:38:42 +0200 Subject: [PATCH 5/9] chore: cargo +nightly fmt --- rust/operator-binary/src/zk_controller.rs | 17 +++++++---------- .../src/zk_controller/validate.rs | 4 +--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/rust/operator-binary/src/zk_controller.rs b/rust/operator-binary/src/zk_controller.rs index 37c1496b..96e49ee3 100644 --- a/rust/operator-binary/src/zk_controller.rs +++ b/rust/operator-binary/src/zk_controller.rs @@ -30,10 +30,7 @@ use stackable_operator::{ }, cli::OperatorEnvironmentOptions, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::{ - product_image_selection::ResolvedProductImage, - rbac::build_rbac_resources, - }, + commons::{product_image_selection::ResolvedProductImage, rbac::build_rbac_resources}, constants::RESTART_CONTROLLER_ENABLED_LABEL, k8s_openapi::{ DeepMerge, @@ -80,12 +77,12 @@ use crate::{ command::create_init_container_command_args, config::jvm::{construct_non_heap_jvm_args, construct_zk_server_heap_env}, crd::{ - JMX_METRICS_PORT_NAME, JVM_SECURITY_PROPERTIES_FILE, - MAX_PREPARE_LOG_FILE_SIZE, MAX_ZK_LOG_FILES_SIZE, METRICS_PROVIDER_HTTP_PORT_NAME, - STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, - STACKABLE_RW_CONFIG_DIR, ZOOKEEPER_ELECTION_PORT, ZOOKEEPER_ELECTION_PORT_NAME, - ZOOKEEPER_LEADER_PORT, ZOOKEEPER_LEADER_PORT_NAME, ZOOKEEPER_PROPERTIES_FILE, - ZOOKEEPER_SERVER_PORT_NAME, ZookeeperRole, + JMX_METRICS_PORT_NAME, JVM_SECURITY_PROPERTIES_FILE, MAX_PREPARE_LOG_FILE_SIZE, + MAX_ZK_LOG_FILES_SIZE, METRICS_PROVIDER_HTTP_PORT_NAME, STACKABLE_CONFIG_DIR, + STACKABLE_DATA_DIR, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, STACKABLE_RW_CONFIG_DIR, + ZOOKEEPER_ELECTION_PORT, ZOOKEEPER_ELECTION_PORT_NAME, ZOOKEEPER_LEADER_PORT, + ZOOKEEPER_LEADER_PORT_NAME, ZOOKEEPER_PROPERTIES_FILE, ZOOKEEPER_SERVER_PORT_NAME, + ZookeeperRole, security::{self, ZookeeperSecurity}, v1alpha1::{self, ZookeeperServerRoleConfig}, }, diff --git a/rust/operator-binary/src/zk_controller/validate.rs b/rust/operator-binary/src/zk_controller/validate.rs index be9b9866..9f1fe157 100644 --- a/rust/operator-binary/src/zk_controller/validate.rs +++ b/rust/operator-binary/src/zk_controller/validate.rs @@ -17,9 +17,7 @@ use stackable_operator::{ use crate::{ crd::{ CONTAINER_IMAGE_BASE_NAME, JVM_SECURITY_PROPERTIES_FILE, ZOOKEEPER_PROPERTIES_FILE, - ZookeeperRole, - security::ZookeeperSecurity, - v1alpha1, + ZookeeperRole, security::ZookeeperSecurity, v1alpha1, }, zk_controller::dereference::DereferencedObjects, }; From ab13256ba7d44ea59f1906924be2a8f9fe954edf Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 18:47:08 +0200 Subject: [PATCH 6/9] chore(znode_controller): drop transitional comment on dereference call --- rust/operator-binary/src/znode_controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/operator-binary/src/znode_controller.rs b/rust/operator-binary/src/znode_controller.rs index 669e9f7d..91631c8c 100644 --- a/rust/operator-binary/src/znode_controller.rs +++ b/rust/operator-binary/src/znode_controller.rs @@ -197,7 +197,7 @@ pub async fn reconcile_znode( }; let client = &ctx.client; - // dereference (client required) — replaces find_zk_of_znode + // dereference (client required) let dereferenced_objects = dereference::dereference(client, znode).await; let mut default_status_updates: Option = None; // Store the znode path in the status rather than the object itself, to ensure that only K8s administrators can override it From 10aaf388bcfe696806d367d817cd80fba995eece Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 19:17:41 +0200 Subject: [PATCH 7/9] fix: update crate hashes --- Cargo.nix | 18 +++++++++--------- crate-hashes.json | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index 64104b87..dcbe2f3c 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4811,7 +4811,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "k8s_version"; authors = [ @@ -9333,7 +9333,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_certs"; authors = [ @@ -9436,7 +9436,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_operator"; authors = [ @@ -9616,7 +9616,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"; @@ -9651,7 +9651,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_shared"; authors = [ @@ -9732,7 +9732,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_telemetry"; authors = [ @@ -9842,7 +9842,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; - sha256 = "0d58yvxvy8hbai12bjhcyvh4zw182j5dsfyqja4k2xc1vzjy29by"; + sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_versioned"; authors = [ @@ -9892,7 +9892,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"; @@ -9960,7 +9960,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 1fe8cd24a174cf426399a777c8b21637618ece17 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 19:46:14 +0200 Subject: [PATCH 8/9] feat: add smoke test snapshot --- tests/templates/kuttl/smoke/10-assert.yaml | 8 + tests/templates/kuttl/smoke/10-assert.yaml.j2 | 123 ------ tests/templates/kuttl/smoke/11-assert.yaml | 15 +- tests/templates/kuttl/smoke/13-assert.yaml.j2 | 371 ++++++++++++++++++ tests/templates/kuttl/smoke/14-assert.yaml.j2 | 258 ++++++++++++ 5 files changed, 644 insertions(+), 131 deletions(-) create mode 100644 tests/templates/kuttl/smoke/10-assert.yaml delete mode 100644 tests/templates/kuttl/smoke/10-assert.yaml.j2 create mode 100644 tests/templates/kuttl/smoke/13-assert.yaml.j2 create mode 100644 tests/templates/kuttl/smoke/14-assert.yaml.j2 diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml new file mode 100644 index 00000000..1b6cb482 --- /dev/null +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: install-test-zk +timeout: 300 +commands: + - script: kubectl -n $NAMESPACE wait --for=condition=available=true zookeeperclusters.zookeeper.stackable.tech/test-zk --timeout 301s diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 deleted file mode 100644 index e6b798ed..00000000 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ /dev/null @@ -1,123 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -metadata: - name: install-test-zk -timeout: 600 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-zk-server-primary - generation: 1 # There should be no unneeded Pod restarts - labels: - restarter.stackable.tech/enabled: "true" -spec: - template: - spec: - containers: - - name: zookeeper - resources: - limits: - cpu: 500m - memory: 512Mi - requests: - cpu: 250m - memory: 512Mi -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - name: vector -{% endif %} - terminationGracePeriodSeconds: 120 -status: - readyReplicas: 2 - replicas: 2 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: test-zk-server-secondary -spec: - template: - spec: - containers: - - name: zookeeper - resources: - limits: - cpu: 600m # From podOverrides - memory: 512Mi - requests: - cpu: 300m # From podOverrides - memory: 512Mi -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - name: vector -{% endif %} - terminationGracePeriodSeconds: 120 -status: - readyReplicas: 1 - replicas: 1 ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: data-test-zk-server-primary-0 -spec: - resources: - requests: - storage: 1Gi -status: - phase: Bound ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: data-test-zk-server-secondary-0 -spec: - resources: - requests: - storage: 2Gi -status: - phase: Bound ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: test-zk-server -status: - expectedPods: 3 - currentHealthy: 3 - disruptionsAllowed: 1 ---- -apiVersion: v1 -kind: Service -metadata: - name: test-zk-server -spec: - type: ClusterIP # listenerClass: cluster-internal ---- -apiVersion: v1 -kind: Service -metadata: - name: test-zk-server-primary-headless -spec: - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - name: test-zk-server-primary-metrics -spec: - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - name: test-zk-server-secondary-headless -spec: - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - name: test-zk-server-secondary-metrics -spec: - type: ClusterIP diff --git a/tests/templates/kuttl/smoke/11-assert.yaml b/tests/templates/kuttl/smoke/11-assert.yaml index 151e7f78..1de89bcc 100644 --- a/tests/templates/kuttl/smoke/11-assert.yaml +++ b/tests/templates/kuttl/smoke/11-assert.yaml @@ -1,19 +1,18 @@ --- apiVersion: kuttl.dev/v1beta1 kind: TestAssert -timeout: 600 +timeout: 60 commands: # # Test envOverrides # + # configOverrides are covered by the ConfigMap data snapshot in 14-assert.yaml.j2; + # env overrides are not, because the operator emits MYID_OFFSET into the + # StatefulSet env array (kuttl matches arrays positionally), so a shape-based + # assertion in 13-assert.yaml.j2 wouldn't be stable. These targeted yq checks + # stay here. + # - script: | kubectl -n $NAMESPACE get sts test-zk-server-primary -o yaml | yq -e '.spec.template.spec.containers[] | select (.name == "zookeeper") | .env[] | select (.name == "COMMON_VAR" and .value == "group-value")' kubectl -n $NAMESPACE get sts test-zk-server-primary -o yaml | yq -e '.spec.template.spec.containers[] | select (.name == "zookeeper") | .env[] | select (.name == "GROUP_VAR" and .value == "group-value")' kubectl -n $NAMESPACE get sts test-zk-server-primary -o yaml | yq -e '.spec.template.spec.containers[] | select (.name == "zookeeper") | .env[] | select (.name == "ROLE_VAR" and .value == "role-value")' - # - # Test configOverrides - # - - script: | - kubectl -n $NAMESPACE get cm test-zk-server-primary -o yaml | yq -e '.data."zoo.cfg"' | grep "prop.common=group" - kubectl -n $NAMESPACE get cm test-zk-server-primary -o yaml | yq -e '.data."zoo.cfg"' | grep "prop.group=group" - kubectl -n $NAMESPACE get cm test-zk-server-primary -o yaml | yq -e '.data."zoo.cfg"' | grep "prop.role=role" diff --git a/tests/templates/kuttl/smoke/13-assert.yaml.j2 b/tests/templates/kuttl/smoke/13-assert.yaml.j2 new file mode 100644 index 00000000..1aa0c0c9 --- /dev/null +++ b/tests/templates/kuttl/smoke/13-assert.yaml.j2 @@ -0,0 +1,371 @@ +{# Templating flags used throughout this file. #} +{% set use_client_tls = test_scenario['values']['use-server-tls'] == 'true' + or test_scenario['values']['use-client-auth-tls'] == 'true' %} +{% set use_client_auth = test_scenario['values']['use-client-auth-tls'] == 'true' %} +{% set vector_enabled = lookup('env', 'VECTOR_AGGREGATOR') | length > 0 %} +{% set zk_client_port = 2282 if use_client_tls else 2181 %} +--- +# Declarative shape assertions for every operator-managed resource in the smoke +# test except ConfigMap *.data* (covered in 14-assert.yaml.j2). +# +# kuttl performs subset matching: any field omitted here is not checked, and +# the live object may carry additional keys/labels. We therefore omit fields +# that are random per install (uids, resourceVersion, clusterIP, and the +# `listener.stackable.tech/mnt.` selector on the cluster-internal Service). +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 60 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-zk-server-primary + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: primary + restarter.stackable.tech/enabled: "true" + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +spec: + podManagementPolicy: Parallel + replicas: 2 + serviceName: test-zk-server-primary-headless + template: + metadata: + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: primary + stackable.tech/vendor: Stackable + spec: + serviceAccount: test-zk-serviceaccount + serviceAccountName: test-zk-serviceaccount + terminationGracePeriodSeconds: 120 + containers: + - name: zookeeper + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 512Mi +{% if vector_enabled %} + - name: vector +{% endif %} +status: + readyReplicas: 2 + replicas: 2 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-zk-server-secondary + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: secondary + restarter.stackable.tech/enabled: "true" + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +spec: + podManagementPolicy: Parallel + replicas: 1 + serviceName: test-zk-server-secondary-headless + template: + spec: + serviceAccount: test-zk-serviceaccount + serviceAccountName: test-zk-serviceaccount + terminationGracePeriodSeconds: 120 + containers: + - name: zookeeper + resources: + limits: + cpu: 600m # From podOverrides + memory: 512Mi + requests: + cpu: 300m # From podOverrides + memory: 512Mi +{% if vector_enabled %} + - name: vector +{% endif %} +status: + readyReplicas: 1 + replicas: 1 +--- +# Cluster-internal listener Service. `selector` is intentionally not asserted: +# it carries a per-install `listener.stackable.tech/mnt.` key. +apiVersion: v1 +kind: Service +metadata: + name: test-zk-server + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk-server + app.kubernetes.io/managed-by: listeners.stackable.tech_listener + app.kubernetes.io/name: listener + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: listeners.stackable.tech/v1alpha1 + controller: true + kind: Listener + name: test-zk-server +spec: + type: ClusterIP + ports: + - name: zk + port: {{ zk_client_port }} + protocol: TCP + targetPort: {{ zk_client_port }} +--- +apiVersion: v1 +kind: Service +metadata: + name: test-zk-server-primary-headless + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: primary + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +spec: + clusterIP: None + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: primary + ports: + - name: zk-leader + port: 2888 + protocol: TCP + targetPort: 2888 + - name: zk-election + port: 3888 + protocol: TCP + targetPort: 3888 +--- +apiVersion: v1 +kind: Service +metadata: + name: test-zk-server-primary-metrics + annotations: + prometheus.io/path: /metrics + prometheus.io/port: "7000" + prometheus.io/scheme: http + prometheus.io/scrape: "true" + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: primary + prometheus.io/scrape: "true" + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +spec: + clusterIP: None + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: primary + ports: + - name: jmx-metrics + port: 9505 + protocol: TCP + targetPort: 9505 + - name: metrics + port: 7000 + protocol: TCP + targetPort: 7000 +--- +apiVersion: v1 +kind: Service +metadata: + name: test-zk-server-secondary-headless + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: secondary + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +spec: + clusterIP: None + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: secondary + ports: + - name: zk-leader + port: 2888 + protocol: TCP + targetPort: 2888 + - name: zk-election + port: 3888 + protocol: TCP + targetPort: 3888 +--- +apiVersion: v1 +kind: Service +metadata: + name: test-zk-server-secondary-metrics + annotations: + prometheus.io/path: /metrics + prometheus.io/port: "7000" + prometheus.io/scheme: http + prometheus.io/scrape: "true" + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: secondary + prometheus.io/scrape: "true" + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +spec: + clusterIP: None + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/name: zookeeper + app.kubernetes.io/role-group: secondary + ports: + - name: jmx-metrics + port: 9505 + protocol: TCP + targetPort: 9505 + - name: metrics + port: 7000 + protocol: TCP + targetPort: 7000 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-zk-server + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +spec: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-zk + app.kubernetes.io/name: zookeeper +status: + currentHealthy: 3 + disruptionsAllowed: 1 + expectedPods: 3 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-zk-serviceaccount + labels: + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper + ownerReferences: + - apiVersion: zookeeper.stackable.tech/v1alpha1 + controller: true + kind: ZookeeperCluster + name: test-zk +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: test-zk-rolebinding + labels: + app.kubernetes.io/instance: test-zk + app.kubernetes.io/managed-by: zookeeper.stackable.tech_zookeepercluster + app.kubernetes.io/name: zookeeper +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: zookeeper-clusterrole +subjects: + - kind: ServiceAccount + name: test-zk-serviceaccount +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: data-test-zk-server-primary-0 +spec: + resources: + requests: + storage: 1Gi +status: + phase: Bound +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: data-test-zk-server-primary-1 +spec: + resources: + requests: + storage: 1Gi +status: + phase: Bound +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: data-test-zk-server-secondary-0 +spec: + resources: + requests: + storage: 2Gi +status: + phase: Bound diff --git a/tests/templates/kuttl/smoke/14-assert.yaml.j2 b/tests/templates/kuttl/smoke/14-assert.yaml.j2 new file mode 100644 index 00000000..17448d5e --- /dev/null +++ b/tests/templates/kuttl/smoke/14-assert.yaml.j2 @@ -0,0 +1,258 @@ +{# Templating flags used throughout this file. #} +{% set use_client_tls = test_scenario['values']['use-server-tls'] == 'true' + or test_scenario['values']['use-client-auth-tls'] == 'true' %} +{% set use_client_auth = test_scenario['values']['use-client-auth-tls'] == 'true' %} +{% set zk_client_port = 2282 if use_client_tls else 2181 %} +--- +# Snapshot the full `.data` of each operator-managed ConfigMap. +# Any code change that alters rendered config values will fail these diffs. +# +# Runs as its own step (after 13) so kuttl does not re-evaluate the heavy +# heredocs on every 1-second readiness retry of the install step. By this +# point the cluster is in steady state, so each script runs once. +# +# The heredoc is quoted (`<<'YAMLEOF'`) so shell substitution is disabled and +# property-file escapes like `\:` survive verbatim. `__NAMESPACE__` is +# substituted afterwards via `sed`, and for the test-znode CM also +# `__ZNODE_UID__`, looked up at runtime from the ZookeeperZnode resource. +# Both sides are normalized to canonical JSON via `yq -o=json`; keys are +# already alphabetical on both sides (operator stores BTreeMap; kubectl +# serializes maps sorted; the heredoc is hand-sorted). +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 60 +commands: + - script: | + expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json + ZOOKEEPER: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }} + ZOOKEEPER_CHROOT: / + ZOOKEEPER_CLIENT_PORT: "{{ zk_client_port }}" + ZOOKEEPER_HOSTS: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }} + YAMLEOF + ) + actual=$(kubectl -n $NAMESPACE get cm test-zk -o yaml | yq -o=json '.data') + if [ "$expected" != "$actual" ]; then + echo "ERROR: ConfigMap test-zk data drifted from snapshot." + echo "=== expected ===" + printf '%s\n' "$expected" + echo "=== actual ===" + printf '%s\n' "$actual" + exit 1 + fi + - script: | + ZNODE_UID=$(kubectl -n $NAMESPACE get zookeeperznode test-znode -o jsonpath='{.metadata.uid}') + expected=$(cat <<'YAMLEOF' | sed -e "s|__NAMESPACE__|$NAMESPACE|g" -e "s|__ZNODE_UID__|$ZNODE_UID|g" | yq -o=json + ZOOKEEPER: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }}/znode-__ZNODE_UID__ + ZOOKEEPER_CHROOT: /znode-__ZNODE_UID__ + ZOOKEEPER_CLIENT_PORT: "{{ zk_client_port }}" + ZOOKEEPER_HOSTS: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }} + YAMLEOF + ) + actual=$(kubectl -n $NAMESPACE get cm test-znode -o yaml | yq -o=json '.data') + if [ "$expected" != "$actual" ]; then + echo "ERROR: ConfigMap test-znode data drifted from snapshot." + echo "=== expected ===" + printf '%s\n' "$expected" + echo "=== actual ===" + printf '%s\n' "$actual" + exit 1 + fi + - script: | + expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json + logback.xml: | + + + + %d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n + + + INFO + + + + + /stackable/log/zookeeper/zookeeper.log4j.xml + + + + + INFO + + + 1 + 1 + /stackable/log/zookeeper/zookeeper.log4j.xml.%i + + + 5MB + + 5000 + + + + + + + + + + + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=5 + zoo.cfg: | + admin.serverPort=8080 + authProvider.x509=org.apache.zookeeper.server.auth.X509AuthenticationProvider +{% if use_client_tls %} + client.portUnification=true +{% endif %} + clientPort={{ zk_client_port }} + dataDir=/stackable/data + initLimit=5 + metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider + metricsProvider.httpPort=7000 + prop.common=group + prop.group=group + prop.role=role + server.10=test-zk-server-primary-0.test-zk-server-primary-headless.__NAMESPACE__.svc.cluster.local\:2888\:3888;{{ zk_client_port }} + server.11=test-zk-server-primary-1.test-zk-server-primary-headless.__NAMESPACE__.svc.cluster.local\:2888\:3888;{{ zk_client_port }} + server.20=test-zk-server-secondary-0.test-zk-server-secondary-headless.__NAMESPACE__.svc.cluster.local\:2888\:3888;{{ zk_client_port }} + serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory +{% if use_client_auth %} + ssl.clientAuth=need +{% endif %} +{% if use_client_tls %} + ssl.hostnameVerification=true + ssl.keyStore.location=/stackable/server_tls/keystore.p12 +{% endif %} + ssl.quorum.clientAuth=need + ssl.quorum.hostnameVerification=true + ssl.quorum.keyStore.location=/stackable/quorum_tls/keystore.p12 + ssl.quorum.trustStore.location=/stackable/quorum_tls/truststore.p12 +{% if use_client_tls %} + ssl.trustStore.location=/stackable/server_tls/truststore.p12 +{% endif %} + sslQuorum=true + syncLimit=2 + tickTime=3000 + YAMLEOF + ) + actual=$(kubectl -n $NAMESPACE get cm test-zk-server-primary -o yaml | yq -o=json '.data') + if [ "$expected" != "$actual" ]; then + echo "ERROR: ConfigMap test-zk-server-primary data drifted from snapshot." + echo "=== expected ===" + printf '%s\n' "$expected" + echo "=== actual ===" + printf '%s\n' "$actual" + exit 1 + fi + - script: | + expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json + logback.xml: | + + + + %d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n + + + INFO + + + + + /stackable/log/zookeeper/zookeeper.log4j.xml + + + + + INFO + + + 1 + 1 + /stackable/log/zookeeper/zookeeper.log4j.xml.%i + + + 5MB + + 5000 + + + + + + + + + + + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=5 + zoo.cfg: | + admin.serverPort=8080 + authProvider.x509=org.apache.zookeeper.server.auth.X509AuthenticationProvider +{% if use_client_tls %} + client.portUnification=true +{% endif %} + clientPort={{ zk_client_port }} + dataDir=/stackable/data + initLimit=5 + metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider + metricsProvider.httpPort=7000 + prop.common=role + prop.role=role + server.10=test-zk-server-primary-0.test-zk-server-primary-headless.__NAMESPACE__.svc.cluster.local\:2888\:3888;{{ zk_client_port }} + server.11=test-zk-server-primary-1.test-zk-server-primary-headless.__NAMESPACE__.svc.cluster.local\:2888\:3888;{{ zk_client_port }} + server.20=test-zk-server-secondary-0.test-zk-server-secondary-headless.__NAMESPACE__.svc.cluster.local\:2888\:3888;{{ zk_client_port }} + serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory +{% if use_client_auth %} + ssl.clientAuth=need +{% endif %} +{% if use_client_tls %} + ssl.hostnameVerification=true + ssl.keyStore.location=/stackable/server_tls/keystore.p12 +{% endif %} + ssl.quorum.clientAuth=need + ssl.quorum.hostnameVerification=true + ssl.quorum.keyStore.location=/stackable/quorum_tls/keystore.p12 + ssl.quorum.trustStore.location=/stackable/quorum_tls/truststore.p12 +{% if use_client_tls %} + ssl.trustStore.location=/stackable/server_tls/truststore.p12 +{% endif %} + sslQuorum=true + syncLimit=2 + tickTime=3000 + YAMLEOF + ) + actual=$(kubectl -n $NAMESPACE get cm test-zk-server-secondary -o yaml | yq -o=json '.data') + if [ "$expected" != "$actual" ]; then + echo "ERROR: ConfigMap test-zk-server-secondary data drifted from snapshot." + echo "=== expected ===" + printf '%s\n' "$expected" + echo "=== actual ===" + printf '%s\n' "$actual" + exit 1 + fi From e877572b35219a21eac0345967aff63e4a15c165 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 19:52:14 +0200 Subject: [PATCH 9/9] docs: adapt changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e06a3cbc..85fb7152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,12 @@ All notable changes to this project will be documented in this file. `security.properties`). Previously, arbitrary file names were silently accepted and ignored ([#1027]). - Bump `stackable-operator` to 0.111.1 ([#1027], [#1028]). +- Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#1034]). [#1020]: https://github.com/stackabletech/zookeeper-operator/pull/1020 [#1027]: https://github.com/stackabletech/zookeeper-operator/pull/1027 [#1028]: https://github.com/stackabletech/zookeeper-operator/pull/1028 +[#1034]: https://github.com/stackabletech/zookeeper-operator/pull/1034 ## [26.3.0] - 2026-03-16