11use std:: { collections:: HashMap , str:: FromStr } ;
22
3+ use snafu:: { ResultExt , Snafu } ;
34use stackable_operator:: {
45 builder:: meta:: ObjectMetaBuilder ,
56 kvp:: { LabelError , Labels } ,
7+ utils:: cluster_info:: KubernetesClusterInfo ,
68 v2:: {
79 builder:: meta:: ownerreference_from_resource,
810 types:: { common:: Port , kubernetes:: ServiceName , operator:: RoleGroupName } ,
@@ -11,7 +13,7 @@ use stackable_operator::{
1113
1214use crate :: {
1315 build_recommended_labels,
14- controller:: ValidatedCluster ,
16+ controller:: { KubernetesResources , ValidatedCluster } ,
1517 crd:: {
1618 HdfsNodeRole , HdfsPodRef ,
1719 constants:: {
@@ -41,6 +43,111 @@ pub mod opa;
4143pub mod properties;
4244pub mod resource;
4345
46+ #[ derive( Snafu , Debug ) ]
47+ pub enum Error {
48+ #[ snafu( display( "failed to build Service for role {role} role group {role_group}" ) ) ]
49+ Service {
50+ source : resource:: service:: Error ,
51+ role : HdfsNodeRole ,
52+ role_group : RoleGroupName ,
53+ } ,
54+
55+ #[ snafu( display( "failed to build ConfigMap for role {role} role group {role_group}" ) ) ]
56+ ConfigMap {
57+ source : resource:: config_map:: Error ,
58+ role : HdfsNodeRole ,
59+ role_group : RoleGroupName ,
60+ } ,
61+
62+ #[ snafu( display( "failed to build StatefulSet for role {role} role group {role_group}" ) ) ]
63+ StatefulSet {
64+ source : resource:: statefulset:: Error ,
65+ role : HdfsNodeRole ,
66+ role_group : RoleGroupName ,
67+ } ,
68+ }
69+
70+ /// Builds every Kubernetes resource for the given validated cluster.
71+ ///
72+ /// Does not need a Kubernetes client: every external reference is already dereferenced and
73+ /// validated by this point, so the errors returned here are resource-assembly failures only.
74+ /// `cluster_info` carries static cluster information resolved at operator startup (e.g. the
75+ /// cluster domain used to build Kerberos principals), not a live client.
76+ ///
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+ ///
80+ /// The resources are returned as flat, unordered collections. The reconcile step re-groups the
81+ /// StatefulSets by role to preserve HDFS's ordered, rollout-gated deployment during upgrades. The
82+ /// discovery `ConfigMap` is deliberately not built here: it needs a live client to resolve
83+ /// listener addresses and is therefore handled in the reconcile step.
84+ pub fn build (
85+ cluster : & ValidatedCluster ,
86+ cluster_info : & KubernetesClusterInfo ,
87+ service_account_name : & str ,
88+ ) -> Result < KubernetesResources , Error > {
89+ let mut services = vec ! [ ] ;
90+ let mut config_maps = vec ! [ ] ;
91+ let mut stateful_sets = vec ! [ ] ;
92+ let mut pod_disruption_budgets = vec ! [ ] ;
93+
94+ for ( role, role_group_configs) in & cluster. role_groups {
95+ for ( role_group_name, rg_config) in role_group_configs {
96+ services. push (
97+ resource:: service:: rolegroup_headless_service ( cluster, role, role_group_name)
98+ . context ( ServiceSnafu {
99+ role : * role,
100+ role_group : role_group_name. clone ( ) ,
101+ } ) ?,
102+ ) ;
103+ services. push (
104+ resource:: service:: rolegroup_metrics_service ( cluster, role, role_group_name)
105+ . context ( ServiceSnafu {
106+ role : * role,
107+ role_group : role_group_name. clone ( ) ,
108+ } ) ?,
109+ ) ;
110+ config_maps. push (
111+ resource:: config_map:: build_rolegroup_config_map (
112+ cluster,
113+ cluster_info,
114+ role,
115+ role_group_name,
116+ )
117+ . context ( ConfigMapSnafu {
118+ role : * role,
119+ role_group : role_group_name. clone ( ) ,
120+ } ) ?,
121+ ) ;
122+ stateful_sets. push (
123+ resource:: statefulset:: build_rolegroup_statefulset (
124+ cluster,
125+ cluster_info,
126+ role,
127+ role_group_name,
128+ rg_config,
129+ service_account_name,
130+ )
131+ . context ( StatefulSetSnafu {
132+ role : * role,
133+ role_group : role_group_name. clone ( ) ,
134+ } ) ?,
135+ ) ;
136+ }
137+
138+ if let Some ( pdb) = resource:: pdb:: build_pdb ( cluster, role) {
139+ pod_disruption_budgets. push ( pdb) ;
140+ }
141+ }
142+
143+ Ok ( KubernetesResources {
144+ services,
145+ config_maps,
146+ pod_disruption_budgets,
147+ stateful_sets,
148+ } )
149+ }
150+
44151/// Builds the [`HdfsPodRef`]s expected for every pod of the given `role`, across all
45152/// of its role groups.
46153///
@@ -263,3 +370,55 @@ fn role_data_ports(role: &HdfsNodeRole, https_enabled: bool) -> Vec<(String, Por
263370 ] ,
264371 }
265372}
373+
374+ #[ cfg( test) ]
375+ mod tests {
376+ use stackable_operator:: kube:: Resource ;
377+
378+ use super :: build;
379+ use crate :: controller:: build:: properties:: test_support:: { cluster_info, validated_cluster} ;
380+
381+ /// The sorted `metadata.name`s of a resource collection.
382+ fn sorted_names ( resources : & [ impl Resource ] ) -> Vec < String > {
383+ let mut names: Vec < String > = resources
384+ . iter ( )
385+ . filter_map ( |resource| resource. meta ( ) . name . clone ( ) )
386+ . collect ( ) ;
387+ names. sort ( ) ;
388+ names
389+ }
390+
391+ /// The aggregator emits, for the minimal three-role cluster (one `default` role group each):
392+ /// one StatefulSet and one ConfigMap per role group, one headless plus one metrics Service per
393+ /// role group, and one default PDB per role.
394+ #[ test]
395+ fn build_produces_expected_resource_names ( ) {
396+ let cluster = validated_cluster ( ) ;
397+ let resources =
398+ build ( & cluster, & cluster_info ( ) , "hdfs-serviceaccount" ) . expect ( "build succeeds" ) ;
399+
400+ assert_eq ! (
401+ sorted_names( & resources. stateful_sets) ,
402+ [
403+ "hdfs-datanode-default" ,
404+ "hdfs-journalnode-default" ,
405+ "hdfs-namenode-default" ,
406+ ]
407+ ) ;
408+ // One headless and one metrics Service per role group.
409+ assert_eq ! ( resources. services. len( ) , 6 ) ;
410+ assert_eq ! (
411+ sorted_names( & resources. config_maps) ,
412+ [
413+ "hdfs-datanode-default" ,
414+ "hdfs-journalnode-default" ,
415+ "hdfs-namenode-default" ,
416+ ]
417+ ) ;
418+ // A default PDB per role.
419+ assert_eq ! (
420+ sorted_names( & resources. pod_disruption_budgets) ,
421+ [ "hdfs-datanode" , "hdfs-journalnode" , "hdfs-namenode" ]
422+ ) ;
423+ }
424+ }
0 commit comments