1- use std:: { collections:: HashMap , str :: FromStr } ;
1+ use std:: collections:: HashMap ;
22
33use snafu:: { ResultExt , Snafu } ;
44use stackable_operator:: {
@@ -7,13 +7,15 @@ use stackable_operator::{
77 utils:: cluster_info:: KubernetesClusterInfo ,
88 v2:: {
99 builder:: meta:: ownerreference_from_resource,
10- types:: { common:: Port , kubernetes :: ServiceName , operator:: RoleGroupName } ,
10+ types:: { common:: Port , operator:: RoleGroupName } ,
1111 } ,
1212} ;
1313
1414use crate :: {
15- build_recommended_labels,
16- controller:: { KubernetesResources , ValidatedCluster } ,
15+ controller:: {
16+ KubernetesResources , ValidatedCluster ,
17+ build:: resource:: rbac:: { build_role_binding, build_service_account} ,
18+ } ,
1719 crd:: {
1820 HdfsNodeRole , HdfsPodRef ,
1921 constants:: {
@@ -32,7 +34,6 @@ use crate::{
3234 SERVICE_PORT_NAME_RPC ,
3335 } ,
3436 } ,
35- hdfs_controller:: RESOURCE_MANAGER_HDFS_CONTROLLER ,
3637} ;
3738
3839pub mod container;
@@ -74,17 +75,13 @@ pub enum Error {
7475/// `cluster_info` carries static cluster information resolved at operator startup (e.g. the
7576/// cluster domain used to build Kerberos principals), not a live client.
7677///
77- /// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under.
78- /// The RBAC resources are built and applied separately in the reconcile step.
79- ///
8078/// The resources are returned as flat, unordered collections. The reconcile step re-groups the
8179/// StatefulSets by role to preserve HDFS's ordered, rollout-gated deployment during upgrades. The
8280/// discovery `ConfigMap` is deliberately not built here: it needs a live client to resolve
8381/// listener addresses and is therefore handled in the reconcile step.
8482pub fn build (
8583 cluster : & ValidatedCluster ,
8684 cluster_info : & KubernetesClusterInfo ,
87- service_account_name : & str ,
8885) -> Result < KubernetesResources , Error > {
8986 let mut services = vec ! [ ] ;
9087 let mut config_maps = vec ! [ ] ;
@@ -126,7 +123,6 @@ pub fn build(
126123 role,
127124 role_group_name,
128125 rg_config,
129- service_account_name,
130126 )
131127 . context ( StatefulSetSnafu {
132128 role : * role,
@@ -145,6 +141,8 @@ pub fn build(
145141 config_maps,
146142 pod_disruption_budgets,
147143 stateful_sets,
144+ service_accounts : vec ! [ build_service_account( cluster) ] ,
145+ role_bindings : vec ! [ build_role_binding( cluster) ] ,
148146 } )
149147}
150148
@@ -168,15 +166,7 @@ pub(crate) fn pod_refs(cluster: &ValidatedCluster, role: &HdfsNodeRole) -> Vec<H
168166 . into_iter ( )
169167 . flatten ( )
170168 . flat_map ( |( role_group_name, role_group) | {
171- // The headless Service that governs the pods is named after the qualified role group
172- // name (see `build::resource::service::rolegroup_headless_service`).
173- let service_name = ServiceName :: from_str (
174- cluster
175- . resource_names ( role, role_group_name)
176- . qualified_role_group_name ( )
177- . as_ref ( ) ,
178- )
179- . expect ( "a qualified role group name is a valid Service name" ) ;
169+ let service_name = cluster. governing_service_name ( role, role_group_name) ;
180170 let object_name = service_name. to_string ( ) ;
181171 let namespace = cluster. namespace . clone ( ) ;
182172 let ports = ports. clone ( ) ;
@@ -191,6 +181,22 @@ pub(crate) fn pod_refs(cluster: &ValidatedCluster, role: &HdfsNodeRole) -> Vec<H
191181 . collect ( )
192182}
193183
184+ /// Returns an [`ObjectMetaBuilder`] pre-filled with the namespace, the resource `name`, an owner
185+ /// reference back to the cluster, and the given recommended `labels`.
186+ pub ( crate ) fn object_meta (
187+ cluster : & ValidatedCluster ,
188+ name : impl Into < String > ,
189+ labels : Labels ,
190+ ) -> ObjectMetaBuilder {
191+ let mut builder = ObjectMetaBuilder :: new ( ) ;
192+ builder
193+ . name_and_namespace ( cluster)
194+ . name ( name)
195+ . ownerreference ( ownerreference_from_resource ( cluster, None , Some ( true ) ) )
196+ . with_labels ( labels) ;
197+ builder
198+ }
199+
194200/// Builds the common [`ObjectMetaBuilder`] shared by a role group's owned resources
195201/// (the ConfigMap and the StatefulSet): name, namespace, owner reference and the
196202/// recommended labels, all derived from the validated cluster.
@@ -203,28 +209,14 @@ pub(crate) fn rolegroup_metadata(
203209 role : & HdfsNodeRole ,
204210 role_group_name : & RoleGroupName ,
205211) -> ObjectMetaBuilder {
206- let role_name = role. to_string ( ) ;
207- let mut metadata = ObjectMetaBuilder :: new ( ) ;
208- metadata
209- . name_and_namespace ( cluster)
210- . name (
211- cluster
212- . resource_names ( role, role_group_name)
213- . qualified_role_group_name ( ) ,
214- )
215- . ownerreference ( ownerreference_from_resource ( cluster, None , Some ( true ) ) )
216- . with_recommended_labels ( & build_recommended_labels (
217- cluster,
218- RESOURCE_MANAGER_HDFS_CONTROLLER ,
219- & cluster. image . app_version_label_value ,
220- & role_name,
221- role_group_name. as_ref ( ) ,
222- ) )
223- . expect (
224- "the recommended labels are valid because the ValidatedCluster uses \
225- fail-safe typed values",
226- ) ;
227- metadata
212+ object_meta (
213+ cluster,
214+ cluster
215+ . role_group_resource_names ( role, role_group_name)
216+ . qualified_role_group_name ( )
217+ . to_string ( ) ,
218+ cluster. recommended_labels ( role, role_group_name) ,
219+ )
228220}
229221
230222/// The rolegroup selector labels (also used as `Service`/`StatefulSet` selectors) for
@@ -394,8 +386,7 @@ mod tests {
394386 #[ test]
395387 fn build_produces_expected_resource_names ( ) {
396388 let cluster = validated_cluster ( ) ;
397- let resources =
398- build ( & cluster, & cluster_info ( ) , "hdfs-serviceaccount" ) . expect ( "build succeeds" ) ;
389+ let resources = build ( & cluster, & cluster_info ( ) ) . expect ( "build succeeds" ) ;
399390
400391 assert_eq ! (
401392 sorted_names( & resources. stateful_sets) ,
@@ -405,8 +396,19 @@ mod tests {
405396 "hdfs-namenode-default" ,
406397 ]
407398 ) ;
408- // One headless and one metrics Service per role group.
409- assert_eq ! ( resources. services. len( ) , 6 ) ;
399+ // One headless (un-suffixed, see `ValidatedCluster::governing_service_name`) and one
400+ // metrics Service per role group.
401+ assert_eq ! (
402+ sorted_names( & resources. services) ,
403+ [
404+ "hdfs-datanode-default" ,
405+ "hdfs-datanode-default-metrics" ,
406+ "hdfs-journalnode-default" ,
407+ "hdfs-journalnode-default-metrics" ,
408+ "hdfs-namenode-default" ,
409+ "hdfs-namenode-default-metrics" ,
410+ ]
411+ ) ;
410412 assert_eq ! (
411413 sorted_names( & resources. config_maps) ,
412414 [
@@ -420,5 +422,34 @@ mod tests {
420422 sorted_names( & resources. pod_disruption_budgets) ,
421423 [ "hdfs-datanode" , "hdfs-journalnode" , "hdfs-namenode" ]
422424 ) ;
425+ // The cluster-shared RBAC pair.
426+ assert_eq ! (
427+ sorted_names( & resources. service_accounts) ,
428+ [ "hdfs-serviceaccount" ]
429+ ) ;
430+ assert_eq ! ( sorted_names( & resources. role_bindings) , [ "hdfs-rolebinding" ] ) ;
431+ }
432+
433+ /// Every StatefulSet's (immutable) `serviceName` must reference a headless Service that the
434+ /// build step actually produces — the pods' DNS names depend on the pair agreeing. Guards the
435+ /// coupling that `ValidatedCluster::governing_service_name` centralises.
436+ #[ test]
437+ fn statefulset_service_name_references_built_service ( ) {
438+ let cluster = validated_cluster ( ) ;
439+ let resources = build ( & cluster, & cluster_info ( ) ) . expect ( "build succeeds" ) ;
440+
441+ let service_names = sorted_names ( & resources. services ) ;
442+ for stateful_set in & resources. stateful_sets {
443+ let service_name = stateful_set
444+ . spec
445+ . as_ref ( )
446+ . and_then ( |spec| spec. service_name . as_deref ( ) )
447+ . expect ( "every StatefulSet sets serviceName" ) ;
448+ assert ! (
449+ service_names. iter( ) . any( |name| name == service_name) ,
450+ "StatefulSet references headless Service {service_name:?}, which is not built \
451+ (built Services: {service_names:?})"
452+ ) ;
453+ }
423454 }
424455}
0 commit comments