Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ All notable changes to this project will be documented in this file.
environment variables by name, so an override replaces the operator's value instead of
producing a duplicated entry whose precedence depended on Kubernetes' duplicate-name
handling ([#1077]).
- Make operations infallible where dependent on static inputs ([#1084]).

### Fixed

Expand All @@ -48,6 +49,7 @@ All notable changes to this project will be documented in this file.
[#1070]: https://github.com/stackabletech/zookeeper-operator/pull/1070
[#1077]: https://github.com/stackabletech/zookeeper-operator/pull/1077
[#1079]: https://github.com/stackabletech/zookeeper-operator/pull/1079
[#1084]: https://github.com/stackabletech/zookeeper-operator/pull/1084

## [26.7.0] - 2026-07-21

Expand Down
5 changes: 3 additions & 2 deletions rust/operator-binary/src/crd/affinity.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use stackable_operator::{
commons::affinity::{StackableAffinityFragment, affinity_between_role_pods},
k8s_openapi::api::core::v1::PodAntiAffinity,
v2::types::operator::ClusterName,
};

use crate::crd::{APP_NAME, ZookeeperRole};

pub fn get_affinity(cluster_name: &str, role: &ZookeeperRole) -> StackableAffinityFragment {
pub fn get_affinity(cluster_name: &ClusterName, role: &ZookeeperRole) -> StackableAffinityFragment {
let affinity_between_role_pods =
affinity_between_role_pods(APP_NAME, cluster_name, role.as_ref(), 70);
affinity_between_role_pods(APP_NAME, cluster_name.as_ref(), role.as_ref(), 70);

StackableAffinityFragment {
pod_affinity: None,
Expand Down
3 changes: 2 additions & 1 deletion rust/operator-binary/src/crd/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ impl DereferencedAuthenticationClasses {
Ok(self.clone())
}

/// USE ONLY IN TESTS! We can not put it behind `#[cfg(test)]` because of <https://github.com/rust-lang/cargo/issues/8379>
/// Test fixture without any AuthenticationClasses.
#[cfg(test)]
pub fn new_for_tests() -> Self {
DereferencedAuthenticationClasses {
dereferenced_authentication_classes: vec![],
Expand Down
106 changes: 79 additions & 27 deletions rust/operator-binary/src/crd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use stackable_operator::{
crd::ClusterRef,
deep_merger::ObjectOverrides,
k8s_openapi::apimachinery::pkg::api::resource::Quantity,
kube::{CustomResource, ResourceExt},
kube::CustomResource,
product_logging::{self, spec::Logging},
role_utils::GenericRoleConfig,
schemars::{self, JsonSchema},
Expand All @@ -31,7 +31,7 @@ use stackable_operator::{
kubernetes::{
ConfigMapName, ListenerClassName, ListenerName, NamespaceName, ServiceName,
},
operator::{OperatorName, ProductName, RoleName},
operator::{ClusterName, OperatorName, ProductName, RoleName},
},
},
versioned::versioned,
Expand All @@ -49,10 +49,37 @@ pub mod tls;
/// exposing the given `zk_role`, `<cluster>-<role>`.
///
/// Lives in the `crd` module (rather than the controller build tree) because it is shared by both
/// controllers and by [`v1alpha1::ZookeeperCluster::server_role_listener_fqdn`].
pub fn role_listener_name(cluster_name: &str, zk_role: &ZookeeperRole) -> ListenerName {
ListenerName::from_str(&format!("{cluster_name}-{role}", role = zk_role.as_ref()))
.expect("the role listener name should be a valid Listener name")
/// controllers and by [`role_listener_fqdn`].
///
/// The returned ListenerName is a lowercase RFC 1035 label name (checked by a unit test).
pub fn role_listener_name(cluster_name: &ClusterName, zk_role: &ZookeeperRole) -> ListenerName {
const _: () = assert!(
ClusterName::MAX_LENGTH + 1 /* dash */ + RoleName::MAX_LENGTH <= ListenerName::MAX_LENGTH,
"The string `<cluster_name>-<role_name>` must not exceed the limit of Listener names."
);
// Both halves are RFC 1123 labels joined by a dash, which is a valid RFC 1123 subdomain.
let _ = ClusterName::IS_RFC_1123_SUBDOMAIN_NAME;
Comment thread
maltesander marked this conversation as resolved.
let _ = RoleName::IS_RFC_1123_LABEL_NAME;

let role_name: &RoleName = zk_role;
ListenerName::from_str(&format!("{cluster_name}-{role_name}"))
.expect("is a valid Listener name")
}

/// The fully-qualified domain name of the role-level
/// [`Listener`](stackable_operator::crd::listener::v1alpha1::Listener) exposing the given
/// `zk_role`, `<cluster>-<role>.<namespace>.svc.<cluster_domain>`.
pub fn role_listener_fqdn(
cluster_name: &ClusterName,
namespace: &NamespaceName,
zk_role: &ZookeeperRole,
cluster_info: &KubernetesClusterInfo,
) -> String {
format!(
"{role_listener_name}.{namespace}.svc.{cluster_domain}",
role_listener_name = role_listener_name(cluster_name, zk_role),
cluster_domain = cluster_info.cluster_domain
)
}

pub const APP_NAME: &str = "zookeeper";
Expand Down Expand Up @@ -87,7 +114,7 @@ pub const STACKABLE_RW_CONFIG_DIR: &str = "/stackable/rwconfig";
pub const CONTAINER_IMAGE_BASE_NAME: &str = "zookeeper";

const DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_minutes_unchecked(2);
pub const DEFAULT_LISTENER_CLASS: &str = "cluster-internal";
constant!(pub DEFAULT_LISTENER_CLASS: ListenerClassName = "cluster-internal");

pub type ZookeeperServerRoleType = Role<
v1alpha1::ZookeeperConfigFragment,
Expand Down Expand Up @@ -315,7 +342,7 @@ pub mod versioned {
}
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Clone, Debug, Eq, EnumIter, Hash, Ord, PartialEq, PartialOrd)]
pub enum ZookeeperRole {
Server,
}
Expand Down Expand Up @@ -358,8 +385,7 @@ fn cluster_config_default() -> v1alpha1::ZookeeperClusterConfig {
}

pub(crate) fn default_listener_class() -> ListenerClassName {
ListenerClassName::from_str(DEFAULT_LISTENER_CLASS)
.expect("the default listener class should be a valid ListenerClass name")
DEFAULT_LISTENER_CLASS.clone()
}

impl Default for ZookeeperServerRoleConfig {
Expand All @@ -380,7 +406,7 @@ impl v1alpha1::ZookeeperConfig {
pub const TICK_TIME: &'static str = "tickTime";

pub(crate) fn default_server_config(
cluster_name: &str,
cluster_name: &ClusterName,
role: &ZookeeperRole,
) -> v1alpha1::ZookeeperConfigFragment {
v1alpha1::ZookeeperConfigFragment {
Expand Down Expand Up @@ -435,21 +461,6 @@ impl ZookeeperPodRef {
}

impl v1alpha1::ZookeeperCluster {
/// The fully-qualified domain name of the role-level [Listener]
///
/// [Listener]: stackable_operator::crd::listener::v1alpha1::Listener
pub fn server_role_listener_fqdn(
&self,
cluster_info: &KubernetesClusterInfo,
) -> Option<String> {
Some(format!(
"{role_listener_name}.{namespace}.svc.{cluster_domain}",
role_listener_name = role_listener_name(&self.name_any(), &ZookeeperRole::Server),
namespace = self.metadata.namespace.as_ref()?,
cluster_domain = cluster_info.cluster_domain
))
}

/// Returns the given role (the `servers` role is required by the CRD).
pub fn role(&self, role_variant: &ZookeeperRole) -> &ZookeeperServerRoleType {
match role_variant {
Expand All @@ -466,7 +477,10 @@ impl v1alpha1::ZookeeperCluster {

#[cfg(test)]
mod tests {
use stackable_operator::versioned::test_utils::RoundtripTestData;
use stackable_operator::{
commons::networking::DomainName, versioned::test_utils::RoundtripTestData,
};
use strum::IntoEnumIterator;

use super::*;

Expand All @@ -476,6 +490,26 @@ mod tests {
let _ = *PRODUCT_NAME;
let _ = *OPERATOR_NAME;
let _ = *SERVER_ROLE_NAME;
let _ = *DEFAULT_LISTENER_CLASS;
}

#[test]
fn role_listener_fqdn_joins_name_namespace_and_cluster_domain() {
let cluster_name = ClusterName::from_str("simple-zookeeper").expect("valid cluster name");
let namespace = NamespaceName::from_str("default").expect("valid namespace");
let cluster_info = KubernetesClusterInfo {
cluster_domain: DomainName::from_str("cluster.local").expect("valid domain"),
};

assert_eq!(
role_listener_fqdn(
&cluster_name,
&namespace,
&ZookeeperRole::Server,
&cluster_info
),
"simple-zookeeper-server.default.svc.cluster.local"
);
}

fn get_server_secret_class(zk: &v1alpha1::ZookeeperCluster) -> Option<&str> {
Expand Down Expand Up @@ -743,4 +777,22 @@ mod tests {
.expect("Failed to parse ZookeeperZnodeSpec YAML")
}
}

#[test]
fn role_listener_name_is_rfc_1035_label_name() {
// Every ClusterName is a valid RFC 1035 label name, so we use just some string with maximum
// length.
let _ = ClusterName::IS_RFC_1035_LABEL_NAME;
let cluster_name = ClusterName::from_str_unsafe(&"a".repeat(ClusterName::MAX_LENGTH));

for role in ZookeeperRole::iter() {
let role_listener_name = role_listener_name(&cluster_name, &role);
assert!(
stackable_operator::validation::is_lowercase_rfc_1035_label(
role_listener_name.as_ref()
)
.is_ok()
);
}
}
}
28 changes: 9 additions & 19 deletions rust/operator-binary/src/crd/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ pub enum Error {

#[snafu(display("failed to add needed volume"))]
AddVolume { source: builder::pod::Error },

#[snafu(display("failed to add needed volumeMount"))]
AddVolumeMount {
source: builder::pod::container::Error,
},
}

/// Helper struct combining TLS settings for server and quorum with the resolved AuthenticationClasses
Expand Down Expand Up @@ -151,6 +146,11 @@ impl ZookeeperSecurity {

/// Adds required volumes and volume mounts to the pod and container builders
/// depending on the tls and authentication settings.
///
/// # Panics
///
/// Panics if the volume mounts cannot be added to the container builder. Only call this on a
/// container builder whose mount paths are still distinct from the ones added here.
pub fn add_volume_mounts(
&self,
pod_builder: &mut PodBuilder,
Expand All @@ -162,7 +162,9 @@ impl ZookeeperSecurity {
if let Some(secret_class) = tls_secret_class {
cb_zookeeper
.add_volume_mount(&*SERVER_TLS_VOLUME_NAME, Self::SERVER_TLS_DIR)
.context(AddVolumeMountSnafu)?;
.expect(
"The mount paths are statically defined and there should be no duplicates.",
);
pod_builder
.add_volume(Self::create_server_tls_volume(
&SERVER_TLS_VOLUME_NAME,
Expand All @@ -175,7 +177,7 @@ impl ZookeeperSecurity {
// quorum
cb_zookeeper
.add_volume_mount(&*QUORUM_TLS_VOLUME_NAME, Self::QUORUM_TLS_DIR)
.context(AddVolumeMountSnafu)?;
.expect("The mount paths are statically defined and there should be no duplicates.");
pod_builder
.add_volume(Self::create_quorum_tls_volume(
&QUORUM_TLS_VOLUME_NAME,
Expand Down Expand Up @@ -376,18 +378,6 @@ impl ZookeeperSecurity {

Ok(volume)
}

/// USE ONLY IN TESTS! We can not put it behind `#[cfg(test)]` because of <https://github.com/rust-lang/cargo/issues/8379>
pub fn new_for_tests() -> Self {
ZookeeperSecurity {
resolved_authentication_classes: DereferencedAuthenticationClasses::new_for_tests(),
server_secret_class: Some(
SecretClassName::from_str("tls").expect("'tls' is a valid SecretClass name"),
),
quorum_secret_class: SecretClassName::from_str("tls")
.expect("'tls' is a valid SecretClass name"),
}
}
}

#[cfg(test)]
Expand Down
17 changes: 14 additions & 3 deletions rust/operator-binary/src/crd/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use std::str::FromStr;

use serde::{Deserialize, Serialize};
use stackable_operator::{
constant,
schemars::{self, JsonSchema},
v2::types::kubernetes::SecretClassName,
versioned::versioned,
};

const TLS_DEFAULT_SECRET_CLASS: &str = "tls";
constant!(TLS_DEFAULT_SECRET_CLASS: SecretClassName = "tls");

#[versioned(version(name = "v1alpha1"))]
pub mod versioned {
Expand Down Expand Up @@ -53,6 +54,16 @@ pub fn server_tls_default() -> Option<SecretClassName> {

/// Helper methods to provide defaults in the CRDs and tests
pub fn quorum_tls_default() -> SecretClassName {
SecretClassName::from_str(TLS_DEFAULT_SECRET_CLASS)
.expect("the default TLS secret class should be a valid SecretClass name")
TLS_DEFAULT_SECRET_CLASS.clone()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_constants() {
// Test that dereferencing the constant does not panic.
let _ = *TLS_DEFAULT_SECRET_CLASS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn build_role_listener(
listener::v1alpha1::Listener {
metadata: object_meta(
cluster,
role_listener_name(cluster.name.as_ref(), zk_role),
role_listener_name(&cluster.name, zk_role),
recommended_labels_for_role_resources(cluster, zk_role),
)
.build(),
Expand Down
Loading
Loading