Skip to content

Commit 13c61b8

Browse files
feat!: Implement server role listener (#957)
* chore(listener): Update operator role permissions * docs(listener): Update service exposition docs * feat!: Implement server role listener BREAKING: CRD changes .spec.clusterConfig.listenerClass to .spec.servers.roleConfig.listenerClass * chore: Update changelog * docs: Update listener usage * fix: Use the applied listener to build the discover config map Note: This is because it relies on information in the status field * feat: Move ZK ports to headless service * chore: Handle errors properly * docs: Fix doc refs * chore: tidy up metrics ports and use consts * Remove commented code * Apply suggestions from code review Co-authored-by: Malte Sander <contact@maltesander.com> * chore: Update CRD field descriptions * fix: Handle unlikely error instead of crashing * Update rust/operator-binary/src/zk_controller.rs Co-authored-by: Malte Sander <contact@maltesander.com> * chore: Add znode ref to error message * style: Replace empty format interpolations * fix: Remove extra brace --------- Co-authored-by: Malte Sander <contact@maltesander.com>
1 parent ca5c56d commit 13c61b8

File tree

15 files changed

+611
-406
lines changed

15 files changed

+611
-406
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
1111
- Use `--file-log-rotation-period` (or `FILE_LOG_ROTATION_PERIOD`) to configure the frequency of rotation.
1212
- Use `--console-log-format` (or `CONSOLE_LOG_FORMAT`) to set the format to `plain` (default) or `json`.
1313
- Add built-in Prometheus support and expose metrics on `/metrics` path of `native-metrics` port ([#955]).
14+
- BREAKING: Add listener support ([#957]).
1415

1516
### Changed
1617

@@ -47,6 +48,7 @@ All notable changes to this project will be documented in this file.
4748
[#946]: https://github.com/stackabletech/zookeeper-operator/pull/946
4849
[#950]: https://github.com/stackabletech/zookeeper-operator/pull/950
4950
[#955]: https://github.com/stackabletech/zookeeper-operator/pull/955
51+
[#957]: https://github.com/stackabletech/zookeeper-operator/pull/957
5052

5153
## [25.3.0] - 2025-03-21
5254

deploy/helm/zookeeper-operator/crds/crds.yaml

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ spec:
2828
clusterConfig:
2929
default:
3030
authentication: []
31-
listenerClass: cluster-internal
3231
tls:
3332
quorumSecretClass: tls
3433
serverSecretClass: tls
@@ -53,20 +52,6 @@ spec:
5352
- authenticationClass
5453
type: object
5554
type: array
56-
listenerClass:
57-
default: cluster-internal
58-
description: |-
59-
This field controls which type of Service the Operator creates for this ZookeeperCluster:
60-
61-
* cluster-internal: Use a ClusterIP service
62-
63-
* external-unstable: Use a NodePort service
64-
65-
This is a temporary solution with the goal to keep yaml manifests forward compatible. In the future, this setting will control which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) will be used to expose the service, and ListenerClass names will stay the same, allowing for a non-breaking change.
66-
enum:
67-
- cluster-internal
68-
- external-unstable
69-
type: string
7055
tls:
7156
default:
7257
quorumSecretClass: tls
@@ -444,11 +429,16 @@ spec:
444429
x-kubernetes-preserve-unknown-fields: true
445430
roleConfig:
446431
default:
432+
listenerClass: cluster-internal
447433
podDisruptionBudget:
448434
enabled: true
449435
maxUnavailable: null
450436
description: This is a product-agnostic RoleConfig, which is sufficient for most of the products.
451437
properties:
438+
listenerClass:
439+
default: cluster-internal
440+
description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose the ZooKeeper servers.
441+
type: string
452442
podDisruptionBudget:
453443
default:
454444
enabled: true

deploy/helm/zookeeper-operator/templates/roles.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ rules:
107107
verbs:
108108
- create
109109
- patch
110+
- apiGroups:
111+
- listeners.stackable.tech
112+
resources:
113+
- listeners
114+
verbs:
115+
- get
116+
- list
117+
- watch
118+
- patch
119+
- create
120+
- delete
110121
- apiGroups:
111122
- {{ include "operator.name" . }}.stackable.tech
112123
resources:
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
= Service exposition with ListenerClasses
2+
:description: Configure the ZooKeeper service exposure with listener classes: cluster-internal, external-unstable or external-stable
23

34
Apache ZooKeeper offers an API. The Operator deploys a service called `<name>` (where `<name>` is the name of the ZookeeperCluster) through which ZooKeeper can be reached.
45

5-
This service can have either the `cluster-internal` or `external-unstable` type. `external-stable` is not supported for ZooKeeper at the moment.
6-
Read more about the types in the xref:concepts:service-exposition.adoc[service exposition] documentation at platform level.
7-
8-
This is how the listener class is configured:
6+
The operator deploys a xref:listener-operator:listener.adoc[Listener] for the Server pods.
7+
The listener defaults to only being accessible from within the Kubernetes cluster, but this can be changed by setting `.spec.servers.roleConfig.listenerClass`:
98

109
[source,yaml]
1110
----
1211
spec:
13-
clusterConfig:
14-
listenerClass: cluster-internal # <1>
12+
servers:
13+
roleConfig:
14+
listenerClass: external-unstable # <1>
1515
----
16-
<1> The default `cluster-internal` setting.
16+
<1> Specify one of `external-stable`, `external-unstable`, `cluster-internal` (the default setting is `cluster-internal`).

rust/operator-binary/src/config/jvm.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use snafu::{OptionExt, ResultExt, Snafu};
22
use stackable_operator::{
33
memory::{BinaryMultiple, MemoryQuantity},
4-
role_utils::{self, GenericRoleConfig, JavaCommonConfig, JvmArgumentOverrides, Role},
4+
role_utils::{self, JavaCommonConfig, JvmArgumentOverrides, Role},
55
};
66

77
use crate::crd::{
8-
JVM_SECURITY_PROPERTIES_FILE, LOG4J_CONFIG_FILE, LOGBACK_CONFIG_FILE, LoggingFramework,
9-
METRICS_PORT, STACKABLE_CONFIG_DIR, STACKABLE_LOG_CONFIG_DIR,
10-
v1alpha1::{ZookeeperCluster, ZookeeperConfig, ZookeeperConfigFragment},
8+
JMX_METRICS_PORT, JVM_SECURITY_PROPERTIES_FILE, LOG4J_CONFIG_FILE, LOGBACK_CONFIG_FILE,
9+
LoggingFramework, STACKABLE_CONFIG_DIR, STACKABLE_LOG_CONFIG_DIR,
10+
v1alpha1::{
11+
ZookeeperCluster, ZookeeperConfig, ZookeeperConfigFragment, ZookeeperServerRoleConfig,
12+
},
1113
};
1214

1315
const JAVA_HEAP_FACTOR: f32 = 0.8;
@@ -29,15 +31,15 @@ pub enum Error {
2931
/// All JVM arguments.
3032
fn construct_jvm_args(
3133
zk: &ZookeeperCluster,
32-
role: &Role<ZookeeperConfigFragment, GenericRoleConfig, JavaCommonConfig>,
34+
role: &Role<ZookeeperConfigFragment, ZookeeperServerRoleConfig, JavaCommonConfig>,
3335
role_group: &str,
3436
) -> Result<Vec<String>, Error> {
3537
let logging_framework = zk.logging_framework();
3638

3739
let jvm_args = vec![
3840
format!("-Djava.security.properties={STACKABLE_CONFIG_DIR}/{JVM_SECURITY_PROPERTIES_FILE}"),
3941
format!(
40-
"-javaagent:/stackable/jmx/jmx_prometheus_javaagent.jar={METRICS_PORT}:/stackable/jmx/server.yaml"
42+
"-javaagent:/stackable/jmx/jmx_prometheus_javaagent.jar={JMX_METRICS_PORT}:/stackable/jmx/server.yaml"
4143
),
4244
match logging_framework {
4345
LoggingFramework::LOG4J => {
@@ -63,7 +65,7 @@ fn construct_jvm_args(
6365
/// [`construct_zk_server_heap_env`]).
6466
pub fn construct_non_heap_jvm_args(
6567
zk: &ZookeeperCluster,
66-
role: &Role<ZookeeperConfigFragment, GenericRoleConfig, JavaCommonConfig>,
68+
role: &Role<ZookeeperConfigFragment, ZookeeperServerRoleConfig, JavaCommonConfig>,
6769
role_group: &str,
6870
) -> Result<String, Error> {
6971
let mut jvm_args = construct_jvm_args(zk, role, role_group)?;
@@ -99,7 +101,10 @@ fn is_heap_jvm_argument(jvm_argument: &str) -> bool {
99101
#[cfg(test)]
100102
mod tests {
101103
use super::*;
102-
use crate::crd::{ZookeeperRole, v1alpha1::ZookeeperConfig};
104+
use crate::crd::{
105+
ZookeeperRole,
106+
v1alpha1::{ZookeeperConfig, ZookeeperServerRoleConfig},
107+
};
103108

104109
#[test]
105110
fn test_construct_jvm_arguments_defaults() {
@@ -182,7 +187,7 @@ mod tests {
182187
) -> (
183188
ZookeeperCluster,
184189
ZookeeperConfig,
185-
Role<ZookeeperConfigFragment, GenericRoleConfig, JavaCommonConfig>,
190+
Role<ZookeeperConfigFragment, ZookeeperServerRoleConfig, JavaCommonConfig>,
186191
String,
187192
) {
188193
let zookeeper: ZookeeperCluster =

rust/operator-binary/src/crd/mod.rs

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ use stackable_operator::{
3434
};
3535
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
3636

37-
use crate::crd::affinity::get_affinity;
37+
use crate::{
38+
crd::{affinity::get_affinity, v1alpha1::ZookeeperServerRoleConfig},
39+
discovery::build_role_group_headless_service_name,
40+
listener::role_listener_name,
41+
};
3842

3943
pub mod affinity;
4044
pub mod authentication;
@@ -47,8 +51,16 @@ pub const OPERATOR_NAME: &str = "zookeeper.stackable.tech";
4751
pub const ZOOKEEPER_PROPERTIES_FILE: &str = "zoo.cfg";
4852
pub const JVM_SECURITY_PROPERTIES_FILE: &str = "security.properties";
4953

50-
pub const METRICS_PORT: u16 = 9505;
54+
pub const ZOOKEEPER_SERVER_PORT_NAME: &str = "zk";
55+
pub const ZOOKEEPER_LEADER_PORT_NAME: &str = "zk-leader";
56+
pub const ZOOKEEPER_LEADER_PORT: u16 = 2888;
57+
pub const ZOOKEEPER_ELECTION_PORT_NAME: &str = "zk-election";
58+
pub const ZOOKEEPER_ELECTION_PORT: u16 = 3888;
59+
60+
pub const JMX_METRICS_PORT_NAME: &str = "metrics";
61+
pub const JMX_METRICS_PORT: u16 = 9505;
5162
pub const METRICS_PROVIDER_HTTP_PORT_KEY: &str = "metricsProvider.httpPort";
63+
pub const METRICS_PROVIDER_HTTP_PORT_NAME: &str = "native-metrics";
5264
pub const METRICS_PROVIDER_HTTP_PORT: u16 = 7000;
5365

5466
pub const STACKABLE_DATA_DIR: &str = "/stackable/data";
@@ -74,6 +86,7 @@ pub const MAX_PREPARE_LOG_FILE_SIZE: MemoryQuantity = MemoryQuantity {
7486
pub const DOCKER_IMAGE_BASE_NAME: &str = "zookeeper";
7587

7688
const DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_minutes_unchecked(2);
89+
pub const DEFAULT_LISTENER_CLASS: &str = "cluster-internal";
7790

7891
mod built_info {
7992
pub const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -141,7 +154,19 @@ pub mod versioned {
141154

142155
// no doc - it's in the struct.
143156
#[serde(skip_serializing_if = "Option::is_none")]
144-
pub servers: Option<Role<ZookeeperConfigFragment, GenericRoleConfig, JavaCommonConfig>>,
157+
pub servers:
158+
Option<Role<ZookeeperConfigFragment, ZookeeperServerRoleConfig, JavaCommonConfig>>,
159+
}
160+
161+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
162+
#[serde(rename_all = "camelCase")]
163+
pub struct ZookeeperServerRoleConfig {
164+
#[serde(flatten)]
165+
pub common: GenericRoleConfig,
166+
167+
/// This field controls which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html) is used to expose the ZooKeeper servers.
168+
#[serde(default = "default_listener_class")]
169+
pub listener_class: String,
145170
}
146171

147172
#[derive(Clone, Deserialize, Debug, Eq, JsonSchema, PartialEq, Serialize)]
@@ -166,29 +191,6 @@ pub mod versioned {
166191
skip_serializing_if = "Option::is_none"
167192
)]
168193
pub tls: Option<tls::v1alpha1::ZookeeperTls>,
169-
170-
/// This field controls which type of Service the Operator creates for this ZookeeperCluster:
171-
///
172-
/// * cluster-internal: Use a ClusterIP service
173-
///
174-
/// * external-unstable: Use a NodePort service
175-
///
176-
/// This is a temporary solution with the goal to keep yaml manifests forward compatible.
177-
/// In the future, this setting will control which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html)
178-
/// will be used to expose the service, and ListenerClass names will stay the same, allowing for a non-breaking change.
179-
#[serde(default)]
180-
pub listener_class: CurrentlySupportedListenerClasses,
181-
}
182-
183-
// TODO: Temporary solution until listener-operator is finished
184-
#[derive(Clone, Debug, Default, Display, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
185-
#[serde(rename_all = "PascalCase")]
186-
pub enum CurrentlySupportedListenerClasses {
187-
#[default]
188-
#[serde(rename = "cluster-internal")]
189-
ClusterInternal,
190-
#[serde(rename = "external-unstable")]
191-
ExternalUnstable,
192194
}
193195

194196
#[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)]
@@ -346,7 +348,7 @@ pub enum ZookeeperRole {
346348
/// Used for service discovery.
347349
pub struct ZookeeperPodRef {
348350
pub namespace: String,
349-
pub role_group_service_name: String,
351+
pub role_group_headless_service_name: String,
350352
pub pod_name: String,
351353
pub zookeeper_myid: u16,
352354
}
@@ -356,15 +358,18 @@ fn cluster_config_default() -> v1alpha1::ZookeeperClusterConfig {
356358
authentication: vec![],
357359
vector_aggregator_config_map_name: None,
358360
tls: tls::default_zookeeper_tls(),
359-
listener_class: v1alpha1::CurrentlySupportedListenerClasses::default(),
360361
}
361362
}
362363

363-
impl v1alpha1::CurrentlySupportedListenerClasses {
364-
pub fn k8s_service_type(&self) -> String {
365-
match self {
366-
v1alpha1::CurrentlySupportedListenerClasses::ClusterInternal => "ClusterIP".to_string(),
367-
v1alpha1::CurrentlySupportedListenerClasses::ExternalUnstable => "NodePort".to_string(),
364+
fn default_listener_class() -> String {
365+
DEFAULT_LISTENER_CLASS.to_owned()
366+
}
367+
368+
impl Default for ZookeeperServerRoleConfig {
369+
fn default() -> Self {
370+
Self {
371+
listener_class: default_listener_class(),
372+
common: Default::default(),
368373
}
369374
}
370375
}
@@ -506,11 +511,11 @@ impl HasStatusCondition for v1alpha1::ZookeeperCluster {
506511
}
507512

508513
impl ZookeeperPodRef {
509-
pub fn fqdn(&self, cluster_info: &KubernetesClusterInfo) -> String {
514+
pub fn internal_fqdn(&self, cluster_info: &KubernetesClusterInfo) -> String {
510515
format!(
511516
"{pod_name}.{service_name}.{namespace}.svc.{cluster_domain}",
512517
pod_name = self.pod_name,
513-
service_name = self.role_group_service_name,
518+
service_name = self.role_group_headless_service_name,
514519
namespace = self.namespace,
515520
cluster_domain = cluster_info.cluster_domain
516521
)
@@ -541,16 +546,16 @@ impl v1alpha1::ZookeeperCluster {
541546
}
542547
}
543548

544-
/// The name of the role-level load-balanced Kubernetes `Service`
545-
pub fn server_role_service_name(&self) -> Option<String> {
546-
self.metadata.name.clone()
547-
}
548-
549-
/// The fully-qualified domain name of the role-level load-balanced Kubernetes `Service`
550-
pub fn server_role_service_fqdn(&self, cluster_info: &KubernetesClusterInfo) -> Option<String> {
549+
/// The fully-qualified domain name of the role-level [Listener]
550+
///
551+
/// [Listener]: stackable_operator::crd::listener::v1alpha1::Listener
552+
pub fn server_role_listener_fqdn(
553+
&self,
554+
cluster_info: &KubernetesClusterInfo,
555+
) -> Option<String> {
551556
Some(format!(
552-
"{role_service_name}.{namespace}.svc.{cluster_domain}",
553-
role_service_name = self.server_role_service_name()?,
557+
"{role_listener_name}.{namespace}.svc.{cluster_domain}",
558+
role_listener_name = role_listener_name(self, &ZookeeperRole::Server),
554559
namespace = self.metadata.namespace.as_ref()?,
555560
cluster_domain = cluster_info.cluster_domain
556561
))
@@ -560,8 +565,10 @@ impl v1alpha1::ZookeeperCluster {
560565
pub fn role(
561566
&self,
562567
role_variant: &ZookeeperRole,
563-
) -> Result<&Role<v1alpha1::ZookeeperConfigFragment, GenericRoleConfig, JavaCommonConfig>, Error>
564-
{
568+
) -> Result<
569+
&Role<v1alpha1::ZookeeperConfigFragment, ZookeeperServerRoleConfig, JavaCommonConfig>,
570+
Error,
571+
> {
565572
match role_variant {
566573
ZookeeperRole::Server => self.spec.servers.as_ref(),
567574
}
@@ -602,7 +609,7 @@ impl v1alpha1::ZookeeperCluster {
602609
}
603610
}
604611

605-
pub fn role_config(&self, role: &ZookeeperRole) -> Option<&GenericRoleConfig> {
612+
pub fn role_config(&self, role: &ZookeeperRole) -> Option<&ZookeeperServerRoleConfig> {
606613
match role {
607614
ZookeeperRole::Server => self.spec.servers.as_ref().map(|s| &s.role_config),
608615
}
@@ -634,8 +641,10 @@ impl v1alpha1::ZookeeperCluster {
634641
for i in 0..rolegroup.replicas.unwrap_or(1) {
635642
pod_refs.push(ZookeeperPodRef {
636643
namespace: ns.clone(),
637-
role_group_service_name: rolegroup_ref.object_name(),
638-
pod_name: format!("{}-{}", rolegroup_ref.object_name(), i),
644+
role_group_headless_service_name: build_role_group_headless_service_name(
645+
rolegroup_ref.object_name(),
646+
),
647+
pod_name: format!("{role_group}-{i}", role_group = rolegroup_ref.object_name()),
639648
zookeeper_myid: i + myid_offset,
640649
});
641650
}

0 commit comments

Comments
 (0)