22
33use std:: str:: FromStr ;
44
5- use stackable_operator:: v2:: types:: operator:: RoleGroupName ;
5+ use snafu:: { ResultExt , Snafu } ;
6+ use stackable_operator:: {
7+ utils:: cluster_info:: KubernetesClusterInfo , v2:: types:: operator:: RoleGroupName ,
8+ } ;
9+
10+ use crate :: controller:: {
11+ KubernetesResources , ValidatedCluster ,
12+ build:: resource:: {
13+ config_map,
14+ listener:: { build_group_listener, group_listener_name} ,
15+ pdb:: build_pdb,
16+ service:: {
17+ build_rolegroup_headless_service, build_rolegroup_metrics_service,
18+ headless_service_ports,
19+ } ,
20+ statefulset,
21+ } ,
22+ } ;
623
724pub mod command;
825pub mod graceful_shutdown;
@@ -13,4 +30,193 @@ pub mod resource;
1330// Placeholder role-group name used for the recommended labels of a role's group listener.
1431// The group listener is owned by the role (not a single role-group), so there is no real
1532// role-group to attribute it to.
16- stackable_operator:: constant!( pub ( crate ) PLACEHOLDER_LISTENER_ROLE_GROUP : RoleGroupName = "none" ) ;
33+ stackable_operator:: constant!( PLACEHOLDER_LISTENER_ROLE_GROUP : RoleGroupName = "none" ) ;
34+
35+ #[ derive( Snafu , Debug ) ]
36+ pub enum Error {
37+ #[ snafu( display( "failed to build ConfigMap for role group {role_group}" ) ) ]
38+ ConfigMap {
39+ source : config_map:: Error ,
40+ role_group : RoleGroupName ,
41+ } ,
42+
43+ #[ snafu( display( "failed to build StatefulSet for role group {role_group}" ) ) ]
44+ StatefulSet {
45+ source : statefulset:: Error ,
46+ role_group : RoleGroupName ,
47+ } ,
48+ }
49+
50+ /// Builds every Kubernetes resource for the given validated cluster.
51+ ///
52+ /// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
53+ /// dereferenced and validated by this point, so the errors returned here are resource-assembly
54+ /// failures only.
55+ ///
56+ /// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under
57+ /// (RBAC resources are built and applied separately, in the reconcile step).
58+ pub fn build (
59+ cluster : & ValidatedCluster ,
60+ cluster_info : & KubernetesClusterInfo ,
61+ service_account_name : & str ,
62+ ) -> Result < KubernetesResources , Error > {
63+ let mut stateful_sets = vec ! [ ] ;
64+ let mut services = vec ! [ ] ;
65+ let mut listeners = vec ! [ ] ;
66+ let mut config_maps = vec ! [ ] ;
67+ let mut pod_disruption_budgets = vec ! [ ] ;
68+
69+ for ( role, role_group_configs) in & cluster. role_group_configs {
70+ for ( role_group_name, role_group_config) in role_group_configs {
71+ let recommended_labels = cluster. recommended_labels ( role, role_group_name) ;
72+ let selector = cluster. role_group_selector ( role, role_group_name) ;
73+
74+ services. push ( build_rolegroup_headless_service (
75+ cluster,
76+ role,
77+ role_group_name,
78+ & recommended_labels,
79+ selector. clone ( ) . into ( ) ,
80+ headless_service_ports ( cluster) ,
81+ ) ) ;
82+ services. push ( build_rolegroup_metrics_service (
83+ cluster,
84+ role,
85+ role_group_name,
86+ & recommended_labels,
87+ selector. into ( ) ,
88+ ) ) ;
89+ config_maps. push (
90+ config_map:: build_rolegroup_config_map (
91+ cluster,
92+ role,
93+ role_group_name,
94+ cluster_info,
95+ & recommended_labels,
96+ )
97+ . context ( ConfigMapSnafu {
98+ role_group : role_group_name. clone ( ) ,
99+ } ) ?,
100+ ) ;
101+ config_maps. push (
102+ config_map:: build_rolegroup_catalog_config_map (
103+ cluster,
104+ role,
105+ role_group_name,
106+ & recommended_labels,
107+ )
108+ . context ( ConfigMapSnafu {
109+ role_group : role_group_name. clone ( ) ,
110+ } ) ?,
111+ ) ;
112+ stateful_sets. push (
113+ statefulset:: build_rolegroup_statefulset (
114+ cluster,
115+ role,
116+ role_group_name,
117+ role_group_config,
118+ service_account_name,
119+ )
120+ . context ( StatefulSetSnafu {
121+ role_group : role_group_name. clone ( ) ,
122+ } ) ?,
123+ ) ;
124+ }
125+
126+ let Some ( role_config) = cluster. role_config ( role) else {
127+ continue ;
128+ } ;
129+
130+ if let Some ( listener_class) = & role_config. listener_class
131+ && let Some ( listener_group_name) = group_listener_name ( cluster, role)
132+ {
133+ listeners. push ( build_group_listener (
134+ cluster,
135+ cluster. recommended_labels ( role, & PLACEHOLDER_LISTENER_ROLE_GROUP ) ,
136+ listener_class,
137+ listener_group_name,
138+ ) ) ;
139+ }
140+
141+ pod_disruption_budgets. extend ( build_pdb ( & role_config. pdb , cluster, role) ) ;
142+ }
143+
144+ Ok ( KubernetesResources {
145+ stateful_sets,
146+ services,
147+ listeners,
148+ config_maps,
149+ pod_disruption_budgets,
150+ } )
151+ }
152+
153+ #[ cfg( test) ]
154+ mod tests {
155+ use stackable_operator:: {
156+ commons:: networking:: DomainName , kube:: Resource , utils:: cluster_info:: KubernetesClusterInfo ,
157+ } ;
158+
159+ use super :: build;
160+ use crate :: controller:: validated_cluster;
161+
162+ /// Collects the `.metadata.name`s of the given resources, sorted for stable comparison.
163+ fn sorted_names ( resources : & [ impl Resource ] ) -> Vec < & str > {
164+ let mut names: Vec < & str > = resources
165+ . iter ( )
166+ . filter_map ( |resource| resource. meta ( ) . name . as_deref ( ) )
167+ . collect ( ) ;
168+ names. sort ( ) ;
169+ names
170+ }
171+
172+ #[ test]
173+ fn build_produces_expected_resource_names ( ) {
174+ let cluster = validated_cluster ( ) ;
175+ let cluster_info = KubernetesClusterInfo {
176+ cluster_domain : DomainName :: try_from ( "cluster.local" )
177+ . expect ( "cluster.local is a valid domain name" ) ,
178+ } ;
179+
180+ let resources =
181+ build ( & cluster, & cluster_info, "simple-trino-serviceaccount" ) . expect ( "build succeeds" ) ;
182+
183+ // One StatefulSet per role group.
184+ assert_eq ! (
185+ sorted_names( & resources. stateful_sets) ,
186+ [
187+ "simple-trino-coordinator-default" ,
188+ "simple-trino-worker-default" ,
189+ ]
190+ ) ;
191+ // One headless and one metrics Service per role group.
192+ assert_eq ! (
193+ sorted_names( & resources. services) ,
194+ [
195+ "simple-trino-coordinator-default-headless" ,
196+ "simple-trino-coordinator-default-metrics" ,
197+ "simple-trino-worker-default-headless" ,
198+ "simple-trino-worker-default-metrics" ,
199+ ]
200+ ) ;
201+ // A config ConfigMap and a catalog ConfigMap per role group.
202+ assert_eq ! (
203+ sorted_names( & resources. config_maps) ,
204+ [
205+ "simple-trino-coordinator-default" ,
206+ "simple-trino-coordinator-default-catalog" ,
207+ "simple-trino-worker-default" ,
208+ "simple-trino-worker-default-catalog" ,
209+ ]
210+ ) ;
211+ // The coordinator is the only role with a group Listener.
212+ assert_eq ! (
213+ sorted_names( & resources. listeners) ,
214+ [ "simple-trino-coordinator" ]
215+ ) ;
216+ // A default PodDisruptionBudget per role.
217+ assert_eq ! (
218+ sorted_names( & resources. pod_disruption_budgets) ,
219+ [ "simple-trino-coordinator" , "simple-trino-worker" ]
220+ ) ;
221+ }
222+ }
0 commit comments