22
33use 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
77use 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";
2122const DEFAULT_SYNC_LIMIT : & str = "2" ;
2223const 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+
106161impl 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+ }
0 commit comments