1- use std:: { collections:: HashMap , str :: FromStr } ;
1+ use std:: collections:: HashMap ;
22
33use snafu:: { ResultExt , Snafu } ;
44use stackable_operator:: {
55 builder:: meta:: ObjectMetaBuilder ,
66 kvp:: { LabelError , Labels } ,
77 utils:: cluster_info:: KubernetesClusterInfo ,
8- v2:: {
9- builder:: meta:: ownerreference_from_resource,
10- types:: { common:: Port , kubernetes:: ServiceName , operator:: RoleGroupName } ,
11- } ,
8+ v2:: types:: { common:: Port , operator:: RoleGroupName } ,
129} ;
1310
1411use crate :: {
15- build_recommended_labels,
16- controller:: { KubernetesResources , ValidatedCluster } ,
12+ controller:: {
13+ KubernetesResources , ValidatedCluster ,
14+ build:: resource:: rbac:: { build_role_binding, build_service_account} ,
15+ } ,
1716 crd:: {
1817 HdfsNodeRole , HdfsPodRef ,
1918 constants:: {
@@ -32,7 +31,6 @@ use crate::{
3231 SERVICE_PORT_NAME_RPC ,
3332 } ,
3433 } ,
35- hdfs_controller:: RESOURCE_MANAGER_HDFS_CONTROLLER ,
3634} ;
3735
3836pub mod container;
@@ -74,17 +72,13 @@ pub enum Error {
7472/// `cluster_info` carries static cluster information resolved at operator startup (e.g. the
7573/// cluster domain used to build Kerberos principals), not a live client.
7674///
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- ///
8075/// The resources are returned as flat, unordered collections. The reconcile step re-groups the
8176/// StatefulSets by role to preserve HDFS's ordered, rollout-gated deployment during upgrades. The
8277/// discovery `ConfigMap` is deliberately not built here: it needs a live client to resolve
8378/// listener addresses and is therefore handled in the reconcile step.
8479pub fn build (
8580 cluster : & ValidatedCluster ,
8681 cluster_info : & KubernetesClusterInfo ,
87- service_account_name : & str ,
8882) -> Result < KubernetesResources , Error > {
8983 let mut services = vec ! [ ] ;
9084 let mut config_maps = vec ! [ ] ;
@@ -126,7 +120,6 @@ pub fn build(
126120 role,
127121 role_group_name,
128122 rg_config,
129- service_account_name,
130123 )
131124 . context ( StatefulSetSnafu {
132125 role : * role,
@@ -145,6 +138,8 @@ pub fn build(
145138 config_maps,
146139 pod_disruption_budgets,
147140 stateful_sets,
141+ service_accounts : vec ! [ build_service_account( cluster) ] ,
142+ role_bindings : vec ! [ build_role_binding( cluster) ] ,
148143 } )
149144}
150145
@@ -168,15 +163,7 @@ pub(crate) fn pod_refs(cluster: &ValidatedCluster, role: &HdfsNodeRole) -> Vec<H
168163 . into_iter ( )
169164 . flatten ( )
170165 . 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" ) ;
166+ let service_name = cluster. governing_service_name ( role, role_group_name) ;
180167 let object_name = service_name. to_string ( ) ;
181168 let namespace = cluster. namespace . clone ( ) ;
182169 let ports = ports. clone ( ) ;
@@ -203,28 +190,13 @@ pub(crate) fn rolegroup_metadata(
203190 role : & HdfsNodeRole ,
204191 role_group_name : & RoleGroupName ,
205192) -> 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
193+ cluster. object_meta (
194+ cluster
195+ . resource_names ( role, role_group_name)
196+ . qualified_role_group_name ( )
197+ . to_string ( ) ,
198+ cluster. recommended_labels ( role, role_group_name) ,
199+ )
228200}
229201
230202/// The rolegroup selector labels (also used as `Service`/`StatefulSet` selectors) for
@@ -373,10 +345,15 @@ fn role_data_ports(role: &HdfsNodeRole, https_enabled: bool) -> Vec<(String, Por
373345
374346#[ cfg( test) ]
375347mod tests {
348+ use std:: collections:: BTreeMap ;
349+
376350 use stackable_operator:: kube:: Resource ;
377351
378352 use super :: build;
379- use crate :: controller:: build:: properties:: test_support:: { cluster_info, validated_cluster} ;
353+ use crate :: {
354+ controller:: build:: properties:: test_support:: { self , cluster_info, validated_cluster} ,
355+ test_support:: deserialize_and_validate_cluster,
356+ } ;
380357
381358 /// The sorted `metadata.name`s of a resource collection.
382359 fn sorted_names ( resources : & [ impl Resource ] ) -> Vec < String > {
@@ -394,8 +371,7 @@ mod tests {
394371 #[ test]
395372 fn build_produces_expected_resource_names ( ) {
396373 let cluster = validated_cluster ( ) ;
397- let resources =
398- build ( & cluster, & cluster_info ( ) , "hdfs-serviceaccount" ) . expect ( "build succeeds" ) ;
374+ let resources = build ( & cluster, & cluster_info ( ) ) . expect ( "build succeeds" ) ;
399375
400376 assert_eq ! (
401377 sorted_names( & resources. stateful_sets) ,
@@ -405,8 +381,19 @@ mod tests {
405381 "hdfs-namenode-default" ,
406382 ]
407383 ) ;
408- // One headless and one metrics Service per role group.
409- assert_eq ! ( resources. services. len( ) , 6 ) ;
384+ // One headless (un-suffixed, see `ValidatedCluster::governing_service_name`) and one
385+ // metrics Service per role group.
386+ assert_eq ! (
387+ sorted_names( & resources. services) ,
388+ [
389+ "hdfs-datanode-default" ,
390+ "hdfs-datanode-default-metrics" ,
391+ "hdfs-journalnode-default" ,
392+ "hdfs-journalnode-default-metrics" ,
393+ "hdfs-namenode-default" ,
394+ "hdfs-namenode-default-metrics" ,
395+ ]
396+ ) ;
410397 assert_eq ! (
411398 sorted_names( & resources. config_maps) ,
412399 [
@@ -421,4 +408,79 @@ mod tests {
421408 [ "hdfs-datanode" , "hdfs-journalnode" , "hdfs-namenode" ]
422409 ) ;
423410 }
411+
412+ /// Every StatefulSet's (immutable) `serviceName` must reference a headless Service that the
413+ /// build step actually produces — the pods' DNS names depend on the pair agreeing. Guards the
414+ /// coupling that `ValidatedCluster::governing_service_name` centralises.
415+ #[ test]
416+ fn statefulset_service_name_references_built_service ( ) {
417+ let cluster = validated_cluster ( ) ;
418+ let resources = build ( & cluster, & cluster_info ( ) ) . expect ( "build succeeds" ) ;
419+
420+ let service_names = sorted_names ( & resources. services ) ;
421+ for stateful_set in & resources. stateful_sets {
422+ let service_name = stateful_set
423+ . spec
424+ . as_ref ( )
425+ . and_then ( |spec| spec. service_name . as_deref ( ) )
426+ . expect ( "every StatefulSet sets serviceName" ) ;
427+ assert ! (
428+ service_names. iter( ) . any( |name| name == service_name) ,
429+ "StatefulSet references headless Service {service_name:?}, which is not built \
430+ (built Services: {service_names:?})"
431+ ) ;
432+ }
433+ }
434+
435+ /// Locks the RBAC resource names, the roleRef, and the recommended label set against
436+ /// accidental drift. The cluster name deliberately differs from the product name so that
437+ /// swapped `name`/`instance` label values cannot pass unnoticed (the shared fixture is named
438+ /// `hdfs`, which would mask exactly that swap).
439+ #[ test]
440+ fn build_produces_rbac ( ) {
441+ let cluster = deserialize_and_validate_cluster (
442+ & test_support:: MINIMAL_HDFS_YAML . replace ( "name: hdfs" , "name: my-hdfs" ) ,
443+ ) ;
444+ let resources = build ( & cluster, & cluster_info ( ) ) . expect ( "build succeeds" ) ;
445+
446+ assert_eq ! (
447+ sorted_names( & resources. service_accounts) ,
448+ [ "my-hdfs-serviceaccount" ]
449+ ) ;
450+ assert_eq ! (
451+ sorted_names( & resources. role_bindings) ,
452+ [ "my-hdfs-rolebinding" ]
453+ ) ;
454+
455+ let expected_labels = BTreeMap :: from (
456+ [
457+ ( "app.kubernetes.io/component" , "none" ) ,
458+ ( "app.kubernetes.io/instance" , "my-hdfs" ) ,
459+ (
460+ "app.kubernetes.io/managed-by" ,
461+ "hdfs.stackable.tech_hdfs-operator-hdfs-controller" ,
462+ ) ,
463+ ( "app.kubernetes.io/name" , "hdfs" ) ,
464+ ( "app.kubernetes.io/role-group" , "none" ) ,
465+ ( "app.kubernetes.io/version" , "3.4.0-stackable0.0.0-dev" ) ,
466+ ( "stackable.tech/vendor" , "Stackable" ) ,
467+ ]
468+ . map ( |( key, value) | ( key. to_string ( ) , value. to_string ( ) ) ) ,
469+ ) ;
470+ let service_account = resources
471+ . service_accounts
472+ . first ( )
473+ . expect ( "a ServiceAccount is built" ) ;
474+ assert_eq ! (
475+ service_account. metadata. labels,
476+ Some ( expected_labels. clone( ) )
477+ ) ;
478+
479+ let role_binding = resources
480+ . role_bindings
481+ . first ( )
482+ . expect ( "a RoleBinding is built" ) ;
483+ assert_eq ! ( role_binding. metadata. labels, Some ( expected_labels) ) ;
484+ assert_eq ! ( role_binding. role_ref. name, "hdfs-clusterrole" ) ;
485+ }
424486}
0 commit comments