1
+ //! Controller for [`v1alpha1::OpenSearchCluster`]
2
+ //!
3
+ //! The cluster specification is validated, Kubernetes resource specifications are created and
4
+ //! applied and the cluster status is updated.
5
+
1
6
use std:: { collections:: BTreeMap , marker:: PhantomData , str:: FromStr , sync:: Arc } ;
2
7
3
8
use apply:: Applier ;
@@ -28,8 +33,8 @@ use crate::{
28
33
v1alpha1:: { self } ,
29
34
} ,
30
35
framework:: {
31
- ClusterName , ControllerName , HasNamespace , HasObjectName , HasUid , IsLabelValue ,
32
- OperatorName , ProductName , ProductVersion , RoleGroupName , RoleName ,
36
+ ClusterName , ControllerName , HasName , HasUid , NameIsValidLabelValue , NamespaceName ,
37
+ OperatorName , ProductName , ProductVersion , RoleGroupName , RoleName , Uid ,
33
38
role_utils:: { GenericProductSpecificCommonConfig , RoleGroupConfig } ,
34
39
} ,
35
40
} ;
@@ -39,12 +44,17 @@ mod build;
39
44
mod update_status;
40
45
mod validate;
41
46
47
+ /// Names in the controller context which are passed to the submodules of the controller
48
+ ///
49
+ /// The names are not directly defined in [`Context`] because not every submodule requires a
50
+ /// Kubernetes client and unit testing is easier without an unnecessary client.
42
51
pub struct ContextNames {
43
52
pub product_name : ProductName ,
44
53
pub operator_name : OperatorName ,
45
54
pub controller_name : ControllerName ,
46
55
}
47
56
57
+ /// The controller context
48
58
pub struct Context {
49
59
client : stackable_operator:: client:: Client ,
50
60
names : ContextNames ,
@@ -113,6 +123,7 @@ type OpenSearchRoleGroupConfig =
113
123
type OpenSearchNodeResources =
114
124
stackable_operator:: commons:: resources:: Resources < v1alpha1:: StorageConfig > ;
115
125
126
+ /// The validated [`v1alpha1::OpenSearchConfig`]
116
127
#[ derive( Clone , Debug , PartialEq ) ]
117
128
pub struct ValidatedOpenSearchConfig {
118
129
pub affinity : StackableAffinity ,
@@ -122,19 +133,23 @@ pub struct ValidatedOpenSearchConfig {
122
133
pub listener_class : String ,
123
134
}
124
135
125
- // validated and converted to validated and safe types
126
- // no user errors
127
- // not restricted by CRD compliance
136
+ /// The validated [`v1alpha1::OpenSearchCluster`]
137
+ ///
138
+ /// Validated means that there should be no reason for Kubernetes to reject resources generated
139
+ /// from these values. This is usually achieved by using fail-safe types. For instance, the cluster
140
+ /// name is wrapped in the type [`ClusterName`]. This type implements e.g. the function
141
+ /// [`ClusterName::to_label_value`] which returns a valid label value as string. If this function
142
+ /// is used as intended, i.e. to set a label value, and if it is used as late as possible in the
143
+ /// call chain, then chances are high that the resulting Kubernetes resource is valid.
128
144
#[ derive( Clone , Debug , PartialEq ) ]
129
145
pub struct ValidatedCluster {
130
146
metadata : ObjectMeta ,
131
147
pub image : ProductImage ,
132
148
pub product_version : ProductVersion ,
133
149
pub name : ClusterName ,
134
- pub namespace : String ,
135
- pub uid : String ,
150
+ pub namespace : NamespaceName ,
151
+ pub uid : Uid ,
136
152
pub role_config : GenericRoleConfig ,
137
- // "validated" means that labels are valid and no ugly rolegroup name broke them
138
153
pub role_group_configs : BTreeMap < RoleGroupName , OpenSearchRoleGroupConfig > ,
139
154
}
140
155
@@ -143,16 +158,17 @@ impl ValidatedCluster {
143
158
image : ProductImage ,
144
159
product_version : ProductVersion ,
145
160
name : ClusterName ,
146
- namespace : String ,
147
- uid : String ,
161
+ namespace : NamespaceName ,
162
+ uid : impl Into < Uid > ,
148
163
role_config : GenericRoleConfig ,
149
164
role_group_configs : BTreeMap < RoleGroupName , OpenSearchRoleGroupConfig > ,
150
165
) -> Self {
166
+ let uid = uid. into ( ) ;
151
167
ValidatedCluster {
152
168
metadata : ObjectMeta {
153
- name : Some ( name. to_object_name ( ) ) ,
154
- namespace : Some ( namespace. clone ( ) ) ,
155
- uid : Some ( uid. clone ( ) ) ,
169
+ name : Some ( name. to_string ( ) ) ,
170
+ namespace : Some ( namespace. to_string ( ) ) ,
171
+ uid : Some ( uid. to_string ( ) ) ,
156
172
..ObjectMeta :: default ( )
157
173
} ,
158
174
image,
@@ -165,21 +181,25 @@ impl ValidatedCluster {
165
181
}
166
182
}
167
183
184
+ /// Returns the one role name
168
185
pub fn role_name ( ) -> RoleName {
169
186
RoleName :: from_str ( "nodes" ) . expect ( "should be a valid role name" )
170
187
}
171
188
189
+ /// Returns true if only a single OpenSearch node is defined in the cluster
172
190
pub fn is_single_node ( & self ) -> bool {
173
191
self . node_count ( ) == 1
174
192
}
175
193
194
+ /// Returns the sum of the replicas in all role-groups
176
195
pub fn node_count ( & self ) -> u32 {
177
196
self . role_group_configs
178
197
. values ( )
179
198
. map ( |rg| rg. replicas as u32 )
180
199
. sum ( )
181
200
}
182
201
202
+ /// Returns all role-group configurations which contain the given node role
183
203
pub fn role_group_configs_filtered_by_node_role (
184
204
& self ,
185
205
node_role : & v1alpha1:: NodeRole ,
@@ -192,27 +212,20 @@ impl ValidatedCluster {
192
212
}
193
213
}
194
214
195
- impl HasObjectName for ValidatedCluster {
196
- fn to_object_name ( & self ) -> String {
197
- self . name . to_object_name ( )
198
- }
199
- }
200
-
201
- impl HasNamespace for ValidatedCluster {
202
- fn to_namespace ( & self ) -> String {
203
- self . namespace . clone ( )
215
+ impl HasName for ValidatedCluster {
216
+ fn to_name ( & self ) -> String {
217
+ self . name . to_string ( )
204
218
}
205
219
}
206
220
207
221
impl HasUid for ValidatedCluster {
208
- fn to_uid ( & self ) -> String {
222
+ fn to_uid ( & self ) -> Uid {
209
223
self . uid . clone ( )
210
224
}
211
225
}
212
226
213
- impl IsLabelValue for ValidatedCluster {
227
+ impl NameIsValidLabelValue for ValidatedCluster {
214
228
fn to_label_value ( & self ) -> String {
215
- // opinionated!
216
229
self . name . to_label_value ( )
217
230
}
218
231
}
@@ -259,6 +272,13 @@ pub fn error_policy(
259
272
}
260
273
}
261
274
275
+ /// Reconcile function of the OpenSearchCluster controller
276
+ ///
277
+ /// The reconcile function performs the following steps:
278
+ /// 1. Validate the given cluster specification and return a [`ValidatedCluster`] if successful.
279
+ /// 2. Build Kubernetes resource specifications from the validated cluster.
280
+ /// 3. Apply the Kubernetes resource specifications
281
+ /// 4. Update the cluster status
262
282
pub async fn reconcile (
263
283
object : Arc < DeserializeGuard < v1alpha1:: OpenSearchCluster > > ,
264
284
context : Arc < Context > ,
@@ -271,7 +291,7 @@ pub async fn reconcile(
271
291
. map_err ( stackable_operator:: kube:: core:: error_boundary:: InvalidObject :: clone)
272
292
. context ( DeserializeClusterDefinitionSnafu ) ?;
273
293
274
- // dereference (client required)
294
+ // not necessary in this controller: dereference (client required)
275
295
276
296
// validate (no client required)
277
297
let validated_cluster = validate ( & context. names , cluster) . context ( ValidateClusterSnafu ) ?;
@@ -284,14 +304,16 @@ pub async fn reconcile(
284
304
let applied_resources = Applier :: new (
285
305
& context. client ,
286
306
& context. names ,
287
- & validated_cluster,
307
+ & validated_cluster. name ,
308
+ & validated_cluster. namespace ,
309
+ & validated_cluster. uid ,
288
310
apply_strategy,
289
311
)
290
312
. apply ( prepared_resources)
291
313
. await
292
314
. context ( ApplyResourcesSnafu ) ?;
293
315
294
- // create discovery ConfigMap based on the applied resources (client required)
316
+ // not necessary in this controller: create discovery ConfigMap based on the applied resources (client required)
295
317
296
318
// update status (client required)
297
319
update_status ( & context. client , & context. names , cluster, applied_resources)
@@ -301,10 +323,16 @@ pub async fn reconcile(
301
323
Ok ( Action :: await_change ( ) )
302
324
}
303
325
304
- // Marker
326
+ /// Marker for prepared Kubernetes resources which are not applied yet
305
327
struct Prepared ;
328
+ /// Marker for applied Kubernetes resources
306
329
struct Applied ;
307
330
331
+ /// List of all Kubernetes resources produced by this controller
332
+ ///
333
+ /// `T` is a marker that indicates if these resources are only [`Prepared`] or already [`Applied`].
334
+ /// The marker is useful e.g. to ensure that the cluster status is updated based on the applied
335
+ /// resources.
308
336
struct KubernetesResources < T > {
309
337
stateful_sets : Vec < StatefulSet > ,
310
338
services : Vec < Service > ,
@@ -324,13 +352,14 @@ mod tests {
324
352
commons:: affinity:: StackableAffinity , k8s_openapi:: api:: core:: v1:: PodTemplateSpec ,
325
353
role_utils:: GenericRoleConfig ,
326
354
} ;
355
+ use uuid:: uuid;
327
356
328
357
use super :: { Context , OpenSearchRoleGroupConfig , ValidatedCluster } ;
329
358
use crate :: {
330
359
controller:: { OpenSearchNodeResources , ValidatedOpenSearchConfig } ,
331
360
crd:: { NodeRoles , v1alpha1} ,
332
361
framework:: {
333
- ClusterName , OperatorName , ProductVersion , RoleGroupName ,
362
+ ClusterName , NamespaceName , OperatorName , ProductVersion , RoleGroupName ,
334
363
builder:: pod:: container:: EnvVarSet , role_utils:: GenericProductSpecificCommonConfig ,
335
364
} ,
336
365
} ;
@@ -400,8 +429,8 @@ mod tests {
400
429
. expect ( "should be a valid ProductImage structure" ) ,
401
430
ProductVersion :: from_str_unsafe ( "3.1.0" ) ,
402
431
ClusterName :: from_str_unsafe ( "my-opensearch" ) ,
403
- "default" . to_owned ( ) ,
404
- "e6ac237d-a6d4-43a1-8135-f36506110912" . to_owned ( ) ,
432
+ NamespaceName :: from_str_unsafe ( "default" ) ,
433
+ uuid ! ( "e6ac237d-a6d4-43a1-8135-f36506110912" ) ,
405
434
GenericRoleConfig :: default ( ) ,
406
435
[
407
436
(
0 commit comments