Skip to content

Commit a837ec1

Browse files
committed
refactor: compute zoo.cfg server addresses in build, not validate
1 parent 42467ba commit a837ec1

4 files changed

Lines changed: 127 additions & 98 deletions

File tree

rust/operator-binary/src/zk_controller.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,9 @@ pub async fn reconcile_zk(
218218
.context(DereferenceSnafu)?;
219219

220220
// validate (no client required)
221-
let validated_cluster = validate::validate(
222-
zk,
223-
&dereferenced_objects,
224-
&ctx.operator_environment,
225-
&client.kubernetes_cluster_info,
226-
)
227-
.context(ValidateClusterSnafu)?;
221+
let validated_cluster =
222+
validate::validate(zk, &dereferenced_objects, &ctx.operator_environment)
223+
.context(ValidateClusterSnafu)?;
228224

229225
// Names are derived from compile-time constants.
230226
let mut cluster_resources = cluster_resources_new(
@@ -278,6 +274,7 @@ pub async fn reconcile_zk(
278274
);
279275
let rg_configmap = config_map::build_server_rolegroup_config_map(
280276
&validated_cluster,
277+
&client.kubernetes_cluster_info,
281278
rolegroup_name,
282279
rolegroup_config,
283280
)
@@ -423,7 +420,7 @@ pub(crate) mod test_support {
423420
zk
424421
}
425422

426-
fn cluster_info() -> KubernetesClusterInfo {
423+
pub fn cluster_info() -> KubernetesClusterInfo {
427424
KubernetesClusterInfo {
428425
cluster_domain: DomainName::try_from("cluster.local").expect("valid domain"),
429426
}
@@ -445,7 +442,6 @@ pub(crate) mod test_support {
445442
authentication_classes: DereferencedAuthenticationClasses::new_for_tests(),
446443
},
447444
&operator_environment(),
448-
&cluster_info(),
449445
)
450446
.expect("validate should succeed for the test fixture")
451447
}
@@ -460,7 +456,7 @@ mod tests {
460456
};
461457

462458
use super::*;
463-
use crate::zk_controller::test_support::{minimal_zk, validated_cluster};
459+
use crate::zk_controller::test_support::{cluster_info, minimal_zk, validated_cluster};
464460

465461
#[test]
466462
fn test_default_config() {
@@ -613,6 +609,7 @@ mod tests {
613609

614610
config_map::build_server_rolegroup_config_map(
615611
&validated_cluster,
612+
&cluster_info(),
616613
&role_group_name,
617614
rolegroup_config,
618615
)

rust/operator-binary/src/zk_controller/build/properties/zoo_cfg.rs

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
33
use std::collections::BTreeMap;
44

5-
use stackable_operator::v2::types::common::Port;
5+
use stackable_operator::{utils::cluster_info::KubernetesClusterInfo, v2::types::common::Port};
66

77
use crate::{
88
crd::{
99
METRICS_PROVIDER_HTTP_PORT, METRICS_PROVIDER_HTTP_PORT_KEY, STACKABLE_DATA_DIR,
10+
ZOOKEEPER_ELECTION_PORT, ZOOKEEPER_LEADER_PORT, ZookeeperPodRef, ZookeeperRole,
1011
security::ZookeeperSecurity, v1alpha1::ZookeeperConfig,
1112
},
1213
zk_controller::validate::{ValidatedCluster, ZookeeperRoleGroupConfig},
@@ -21,25 +22,65 @@ const DEFAULT_INIT_LIMIT: &str = "5";
2122
const DEFAULT_SYNC_LIMIT: &str = "2";
2223
const DEFAULT_TICK_TIME: &str = "3000";
2324

24-
/// Builds the `zoo.cfg` key/value pairs for a role group.
25+
/// Builds the `server.<myid>` quorum entries for `zoo.cfg` from the expected pods.
26+
///
27+
/// The pods are predicted from the validated role-group configs (`replicas` + `myidOffset`)
28+
/// rather than from the live cluster state, to avoid instance churn.
29+
pub(crate) fn server_addresses(
30+
cluster: &ValidatedCluster,
31+
cluster_info: &KubernetesClusterInfo,
32+
) -> BTreeMap<String, String> {
33+
let security = &cluster.cluster_config.zookeeper_security;
34+
let mut server_addresses = BTreeMap::new();
35+
for (rg_name, rg_config) in cluster
36+
.role_group_configs
37+
.get(&ZookeeperRole::Server)
38+
.into_iter()
39+
.flatten()
40+
{
41+
let resource_names = cluster.resource_names(rg_name);
42+
let headless_service_name = resource_names.headless_service_name();
43+
let stateful_set_name = resource_names.stateful_set_name().to_string();
44+
// An unset replica count (HPA-managed) predicts a single-server quorum entry, matching
45+
// the historical default.
46+
for i in 0..rg_config.replicas.unwrap_or(1) {
47+
let pod_ref = ZookeeperPodRef {
48+
namespace: cluster.namespace.clone(),
49+
role_group_headless_service_name: headless_service_name.clone(),
50+
pod_name: format!("{stateful_set_name}-{i}"),
51+
zookeeper_myid: i + rg_config.config.myid_offset,
52+
};
53+
server_addresses.insert(
54+
format!("server.{id}", id = pod_ref.zookeeper_myid),
55+
format!(
56+
"{internal_fqdn}:{ZOOKEEPER_LEADER_PORT}:{ZOOKEEPER_ELECTION_PORT};{client_port}",
57+
internal_fqdn = pod_ref.internal_fqdn(cluster_info),
58+
client_port = security.client_port()
59+
),
60+
);
61+
}
62+
}
63+
server_addresses
64+
}
65+
66+
/// Builds the `zoo.cfg` key/value pairs for a role group, excluding the
67+
/// `server.<myid>` quorum entries (which depend on `cluster_info`).
2568
///
2669
/// Precedence (lowest to highest):
27-
/// 1. `server.<myid>` quorum entries (precomputed in validate)
28-
/// 2. operator-injected defaults (formerly `product-config` `properties.yaml`)
29-
/// 3. TLS / quorum settings from [`ZookeeperSecurity`]
30-
/// 4. user-set merged config (`initLimit` / `syncLimit` / `tickTime`)
31-
/// 5. `configOverrides` for `zoo.cfg`
32-
pub fn build(
70+
/// 1. operator-injected defaults (formerly `product-config` `properties.yaml`)
71+
/// 2. TLS / quorum settings from [`ZookeeperSecurity`]
72+
/// 3. user-set merged config (`initLimit` / `syncLimit` / `tickTime`)
73+
/// 4. `configOverrides` for `zoo.cfg`
74+
fn build_base(
3375
cluster: &ValidatedCluster,
3476
rolegroup_config: &ZookeeperRoleGroupConfig,
3577
) -> BTreeMap<String, String> {
3678
let security = &cluster.cluster_config.zookeeper_security;
3779
let config = &rolegroup_config.config;
3880

39-
// 1. server.<myid> quorum entries.
40-
let mut zoo_cfg = cluster.cluster_config.server_addresses.clone();
81+
let mut zoo_cfg = BTreeMap::new();
4182

42-
// 2. Operator-injected defaults (former properties.yaml recommended/default
83+
// 1. Operator-injected defaults (former properties.yaml recommended/default
4384
// values and the `Configuration::compute_files` output).
4485
zoo_cfg.insert(
4586
ADMIN_SERVER_PORT_KEY.to_string(),
@@ -74,10 +115,10 @@ pub fn build(
74115
METRICS_PROVIDER_HTTP_PORT.to_string(),
75116
);
76117

77-
// 3. TLS / quorum settings.
118+
// 2. TLS / quorum settings.
78119
zoo_cfg.extend(security.config_settings());
79120

80-
// 4. User-set merged config overrides the seeded defaults above.
121+
// 3. User-set merged config overrides the seeded defaults above.
81122
if let Some(init_limit) = config.init_limit {
82123
zoo_cfg.insert(
83124
ZookeeperConfig::INIT_LIMIT.to_string(),
@@ -97,24 +138,73 @@ pub fn build(
97138
);
98139
}
99140

100-
// 5. configOverrides go last so they win.
141+
// 4. configOverrides go last so they win.
101142
zoo_cfg.extend(rolegroup_config.config_overrides.zoo_cfg.clone());
102143

103144
zoo_cfg
104145
}
105146

147+
/// Builds the full `zoo.cfg` key/value pairs for a role group.
148+
///
149+
/// The `server.<myid>` quorum entries take lowest precedence; everything from
150+
/// [`build_base`] is layered on top.
151+
pub fn build(
152+
cluster: &ValidatedCluster,
153+
rolegroup_config: &ZookeeperRoleGroupConfig,
154+
server_addresses: &BTreeMap<String, String>,
155+
) -> BTreeMap<String, String> {
156+
let mut zoo_cfg = server_addresses.clone();
157+
zoo_cfg.extend(build_base(cluster, rolegroup_config));
158+
zoo_cfg
159+
}
160+
106161
impl ValidatedCluster {
107162
/// Resolves the metrics HTTP port for the given role group, honoring a
108163
/// `metricsProvider.httpPort` `configOverride` if present.
109-
///
110-
/// Defined here (in the build layer) rather than in `validate` so that resolving the port —
111-
/// which renders the full `zoo.cfg` via [`build`] — does not invert the validate → build
112-
/// dependency direction.
113164
pub fn metrics_http_port(&self, rolegroup_config: &ZookeeperRoleGroupConfig) -> Port {
114-
build(self, rolegroup_config)
165+
// The metrics port is independent of the server.<myid> quorum entries.
166+
build_base(self, rolegroup_config)
115167
.get(METRICS_PROVIDER_HTTP_PORT_KEY)
116168
.and_then(|port| port.parse::<u16>().ok())
117169
.map(Port::from)
118170
.unwrap_or(METRICS_PROVIDER_HTTP_PORT)
119171
}
120172
}
173+
174+
#[cfg(test)]
175+
mod tests {
176+
use super::*;
177+
use crate::zk_controller::test_support::{cluster_info, minimal_zk, validated_cluster};
178+
179+
#[test]
180+
fn server_addresses_predicts_one_entry_per_replica() {
181+
let zk = minimal_zk(
182+
r#"
183+
apiVersion: zookeeper.stackable.tech/v1alpha1
184+
kind: ZookeeperCluster
185+
metadata:
186+
name: test-zk
187+
spec:
188+
image:
189+
productVersion: "3.9.5"
190+
servers:
191+
roleGroups:
192+
default:
193+
replicas: 3
194+
"#,
195+
);
196+
let validated = validated_cluster(&zk);
197+
198+
let addresses = server_addresses(&validated, &cluster_info());
199+
200+
// One quorum entry per replica, keyed by `server.<myid>` (myidOffset default 1).
201+
assert_eq!(addresses.len(), 3);
202+
for myid in 1..=3 {
203+
let entry = addresses
204+
.get(&format!("server.{myid}"))
205+
.unwrap_or_else(|| panic!("missing server.{myid}"));
206+
// host:leader:election;client_port — default (non-TLS) client port.
207+
assert!(entry.contains(":2888:3888;"), "unexpected entry: {entry}");
208+
}
209+
}
210+
}

rust/operator-binary/src/zk_controller/build/resource/config_map.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use stackable_operator::{
1212
builder::configmap::ConfigMapBuilder,
1313
k8s_openapi::api::core::v1::ConfigMap,
1414
product_logging::framework::VECTOR_CONFIG_FILE,
15+
utils::cluster_info::KubernetesClusterInfo,
1516
v2::{
1617
config_file_writer::{PropertiesWriterError, to_java_properties_string},
1718
types::operator::RoleGroupName,
@@ -50,20 +51,23 @@ type Result<T, E = Error> = std::result::Result<T, E>;
5051
/// [`ResourceNames`]: stackable_operator::v2::role_group_utils::ResourceNames
5152
pub fn build_server_rolegroup_config_map(
5253
cluster: &ValidatedCluster,
54+
cluster_info: &KubernetesClusterInfo,
5355
role_group_name: &RoleGroupName,
5456
rolegroup_config: &ZookeeperRoleGroupConfig,
5557
) -> Result<ConfigMap> {
5658
let mut data: BTreeMap<String, String> = BTreeMap::new();
5759

5860
// zoo.cfg
61+
let server_addresses = zoo_cfg::server_addresses(cluster, cluster_info);
5962
data.insert(
6063
ConfigFileName::ZooCfg.to_string(),
61-
to_java_properties_string(zoo_cfg::build(cluster, rolegroup_config).iter()).with_context(
62-
|_| SerializePropertiesSnafu {
63-
file: ConfigFileName::ZooCfg.to_string(),
64-
role_group: role_group_name.clone(),
65-
},
66-
)?,
64+
to_java_properties_string(
65+
zoo_cfg::build(cluster, rolegroup_config, &server_addresses).iter(),
66+
)
67+
.with_context(|_| SerializePropertiesSnafu {
68+
file: ConfigFileName::ZooCfg.to_string(),
69+
role_group: role_group_name.clone(),
70+
})?,
6771
);
6872

6973
// security.properties

rust/operator-binary/src/zk_controller/validate.rs

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ use stackable_operator::{
2828
product_logging::spec::Logging,
2929
role_utils::RoleGroup,
3030
shared::time::Duration,
31-
utils::cluster_info::KubernetesClusterInfo,
3231
v2::{
3332
HasName, HasUid, NameIsValidLabelValue,
3433
builder::{
@@ -61,8 +60,7 @@ use strum::IntoEnumIterator;
6160

6261
use crate::{
6362
crd::{
64-
APP_NAME, CONTAINER_IMAGE_BASE_NAME, OPERATOR_NAME, ZOOKEEPER_ELECTION_PORT,
65-
ZOOKEEPER_LEADER_PORT, ZookeeperPodRef, ZookeeperRole, ZookeeperServerRoleType,
63+
APP_NAME, CONTAINER_IMAGE_BASE_NAME, OPERATOR_NAME, ZookeeperRole, ZookeeperServerRoleType,
6664
authentication, default_listener_class,
6765
security::ZookeeperSecurity,
6866
v1alpha1::{self, ZookeeperConfig, ZookeeperConfigOverrides, ZookeeperServerRoleConfig},
@@ -444,10 +442,6 @@ impl Resource for ValidatedCluster {
444442
pub struct ValidatedClusterConfig {
445443
pub zookeeper_security: ZookeeperSecurity,
446444

447-
/// The `server.<myid>` entries for `zoo.cfg`, precomputed from the expected
448-
/// pods so the ConfigMap builder does not need the cluster object.
449-
pub server_addresses: BTreeMap<String, String>,
450-
451445
/// The ListenerClass used to expose the ZooKeeper servers, so the listener builder does not
452446
/// need the raw cluster object.
453447
pub listener_class: ListenerClassName,
@@ -464,7 +458,6 @@ pub fn validate(
464458
zk: &v1alpha1::ZookeeperCluster,
465459
dereferenced_objects: &DereferencedObjects,
466460
operator_environment: &OperatorEnvironmentOptions,
467-
cluster_info: &KubernetesClusterInfo,
468461
) -> Result<ValidatedCluster> {
469462
let image = zk
470463
.spec
@@ -531,14 +524,6 @@ pub fn validate(
531524
.map(|role| role.role_config.listener_class.clone())
532525
.unwrap_or_else(|_| default_listener_class());
533526

534-
let server_addresses = server_addresses(
535-
&name,
536-
&namespace,
537-
&role_group_configs,
538-
&zookeeper_security,
539-
cluster_info,
540-
);
541-
542527
let role_config = zk.role_config(&ZookeeperRole::Server).map(
543528
|ZookeeperServerRoleConfig { common, .. }| ValidatedRoleConfig {
544529
pdb: common.pod_disruption_budget.clone(),
@@ -553,7 +538,6 @@ pub fn validate(
553538
product_version,
554539
ValidatedClusterConfig {
555540
zookeeper_security,
556-
server_addresses,
557541
listener_class,
558542
},
559543
role_config,
@@ -610,52 +594,6 @@ fn validate_role_group_config(
610594
})
611595
}
612596

613-
/// Builds the `server.<myid>` quorum entries for `zoo.cfg` from the expected pods.
614-
///
615-
/// The pods are predicted from the validated role-group configs (`replicas` + `myidOffset`)
616-
/// rather than from the live cluster state, to avoid instance churn.
617-
fn server_addresses(
618-
cluster_name: &ClusterName,
619-
namespace: &NamespaceName,
620-
role_group_configs: &BTreeMap<ZookeeperRole, BTreeMap<RoleGroupName, ZookeeperRoleGroupConfig>>,
621-
zookeeper_security: &ZookeeperSecurity,
622-
cluster_info: &KubernetesClusterInfo,
623-
) -> BTreeMap<String, String> {
624-
let mut server_addresses = BTreeMap::new();
625-
for (rg_name, rg_config) in role_group_configs
626-
.get(&ZookeeperRole::Server)
627-
.into_iter()
628-
.flatten()
629-
{
630-
let resource_names = ResourceNames {
631-
cluster_name: cluster_name.clone(),
632-
role_name: ValidatedCluster::role_name(),
633-
role_group_name: rg_name.clone(),
634-
};
635-
let headless_service_name = resource_names.headless_service_name();
636-
let stateful_set_name = resource_names.stateful_set_name().to_string();
637-
// An unset replica count (HPA-managed) predicts a single-server quorum entry, matching
638-
// the historical default.
639-
for i in 0..rg_config.replicas.unwrap_or(1) {
640-
let pod_ref = ZookeeperPodRef {
641-
namespace: namespace.clone(),
642-
role_group_headless_service_name: headless_service_name.clone(),
643-
pod_name: format!("{stateful_set_name}-{i}"),
644-
zookeeper_myid: i + rg_config.config.myid_offset,
645-
};
646-
server_addresses.insert(
647-
format!("server.{id}", id = pod_ref.zookeeper_myid),
648-
format!(
649-
"{internal_fqdn}:{ZOOKEEPER_LEADER_PORT}:{ZOOKEEPER_ELECTION_PORT};{client_port}",
650-
internal_fqdn = pod_ref.internal_fqdn(cluster_info),
651-
client_port = zookeeper_security.client_port()
652-
),
653-
);
654-
}
655-
}
656-
server_addresses
657-
}
658-
659597
#[cfg(test)]
660598
mod tests {
661599
use stackable_operator::k8s_openapi::apimachinery::pkg::api::resource::Quantity;

0 commit comments

Comments
 (0)