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+
16use std:: { collections:: BTreeMap , marker:: PhantomData , str:: FromStr , sync:: Arc } ;
27
38use apply:: Applier ;
@@ -28,8 +33,8 @@ use crate::{
2833 v1alpha1:: { self } ,
2934 } ,
3035 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 ,
3338 role_utils:: { GenericProductSpecificCommonConfig , RoleGroupConfig } ,
3439 } ,
3540} ;
@@ -39,12 +44,17 @@ mod build;
3944mod update_status;
4045mod validate;
4146
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.
4251pub struct ContextNames {
4352 pub product_name : ProductName ,
4453 pub operator_name : OperatorName ,
4554 pub controller_name : ControllerName ,
4655}
4756
57+ /// The controller context
4858pub struct Context {
4959 client : stackable_operator:: client:: Client ,
5060 names : ContextNames ,
@@ -113,6 +123,7 @@ type OpenSearchRoleGroupConfig =
113123type OpenSearchNodeResources =
114124 stackable_operator:: commons:: resources:: Resources < v1alpha1:: StorageConfig > ;
115125
126+ /// The validated [`v1alpha1::OpenSearchConfig`]
116127#[ derive( Clone , Debug , PartialEq ) ]
117128pub struct ValidatedOpenSearchConfig {
118129 pub affinity : StackableAffinity ,
@@ -122,19 +133,23 @@ pub struct ValidatedOpenSearchConfig {
122133 pub listener_class : String ,
123134}
124135
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.
128144#[ derive( Clone , Debug , PartialEq ) ]
129145pub struct ValidatedCluster {
130146 metadata : ObjectMeta ,
131147 pub image : ProductImage ,
132148 pub product_version : ProductVersion ,
133149 pub name : ClusterName ,
134- pub namespace : String ,
135- pub uid : String ,
150+ pub namespace : NamespaceName ,
151+ pub uid : Uid ,
136152 pub role_config : GenericRoleConfig ,
137- // "validated" means that labels are valid and no ugly rolegroup name broke them
138153 pub role_group_configs : BTreeMap < RoleGroupName , OpenSearchRoleGroupConfig > ,
139154}
140155
@@ -143,16 +158,17 @@ impl ValidatedCluster {
143158 image : ProductImage ,
144159 product_version : ProductVersion ,
145160 name : ClusterName ,
146- namespace : String ,
147- uid : String ,
161+ namespace : NamespaceName ,
162+ uid : impl Into < Uid > ,
148163 role_config : GenericRoleConfig ,
149164 role_group_configs : BTreeMap < RoleGroupName , OpenSearchRoleGroupConfig > ,
150165 ) -> Self {
166+ let uid = uid. into ( ) ;
151167 ValidatedCluster {
152168 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 ( ) ) ,
156172 ..ObjectMeta :: default ( )
157173 } ,
158174 image,
@@ -165,21 +181,25 @@ impl ValidatedCluster {
165181 }
166182 }
167183
184+ /// Returns the one role name
168185 pub fn role_name ( ) -> RoleName {
169186 RoleName :: from_str ( "nodes" ) . expect ( "should be a valid role name" )
170187 }
171188
189+ /// Returns true if only a single OpenSearch node is defined in the cluster
172190 pub fn is_single_node ( & self ) -> bool {
173191 self . node_count ( ) == 1
174192 }
175193
194+ /// Returns the sum of the replicas in all role-groups
176195 pub fn node_count ( & self ) -> u32 {
177196 self . role_group_configs
178197 . values ( )
179198 . map ( |rg| rg. replicas as u32 )
180199 . sum ( )
181200 }
182201
202+ /// Returns all role-group configurations which contain the given node role
183203 pub fn role_group_configs_filtered_by_node_role (
184204 & self ,
185205 node_role : & v1alpha1:: NodeRole ,
@@ -192,27 +212,20 @@ impl ValidatedCluster {
192212 }
193213}
194214
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 ( )
204218 }
205219}
206220
207221impl HasUid for ValidatedCluster {
208- fn to_uid ( & self ) -> String {
222+ fn to_uid ( & self ) -> Uid {
209223 self . uid . clone ( )
210224 }
211225}
212226
213- impl IsLabelValue for ValidatedCluster {
227+ impl NameIsValidLabelValue for ValidatedCluster {
214228 fn to_label_value ( & self ) -> String {
215- // opinionated!
216229 self . name . to_label_value ( )
217230 }
218231}
@@ -259,6 +272,13 @@ pub fn error_policy(
259272 }
260273}
261274
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
262282pub async fn reconcile (
263283 object : Arc < DeserializeGuard < v1alpha1:: OpenSearchCluster > > ,
264284 context : Arc < Context > ,
@@ -271,7 +291,7 @@ pub async fn reconcile(
271291 . map_err ( stackable_operator:: kube:: core:: error_boundary:: InvalidObject :: clone)
272292 . context ( DeserializeClusterDefinitionSnafu ) ?;
273293
274- // dereference (client required)
294+ // not necessary in this controller: dereference (client required)
275295
276296 // validate (no client required)
277297 let validated_cluster = validate ( & context. names , cluster) . context ( ValidateClusterSnafu ) ?;
@@ -284,14 +304,16 @@ pub async fn reconcile(
284304 let applied_resources = Applier :: new (
285305 & context. client ,
286306 & context. names ,
287- & validated_cluster,
307+ & validated_cluster. name ,
308+ & validated_cluster. namespace ,
309+ & validated_cluster. uid ,
288310 apply_strategy,
289311 )
290312 . apply ( prepared_resources)
291313 . await
292314 . context ( ApplyResourcesSnafu ) ?;
293315
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)
295317
296318 // update status (client required)
297319 update_status ( & context. client , & context. names , cluster, applied_resources)
@@ -301,10 +323,16 @@ pub async fn reconcile(
301323 Ok ( Action :: await_change ( ) )
302324}
303325
304- // Marker
326+ /// Marker for prepared Kubernetes resources which are not applied yet
305327struct Prepared ;
328+ /// Marker for applied Kubernetes resources
306329struct Applied ;
307330
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.
308336struct KubernetesResources < T > {
309337 stateful_sets : Vec < StatefulSet > ,
310338 services : Vec < Service > ,
@@ -324,13 +352,14 @@ mod tests {
324352 commons:: affinity:: StackableAffinity , k8s_openapi:: api:: core:: v1:: PodTemplateSpec ,
325353 role_utils:: GenericRoleConfig ,
326354 } ;
355+ use uuid:: uuid;
327356
328357 use super :: { Context , OpenSearchRoleGroupConfig , ValidatedCluster } ;
329358 use crate :: {
330359 controller:: { OpenSearchNodeResources , ValidatedOpenSearchConfig } ,
331360 crd:: { NodeRoles , v1alpha1} ,
332361 framework:: {
333- ClusterName , OperatorName , ProductVersion , RoleGroupName ,
362+ ClusterName , NamespaceName , OperatorName , ProductVersion , RoleGroupName ,
334363 builder:: pod:: container:: EnvVarSet , role_utils:: GenericProductSpecificCommonConfig ,
335364 } ,
336365 } ;
@@ -400,8 +429,8 @@ mod tests {
400429 . expect ( "should be a valid ProductImage structure" ) ,
401430 ProductVersion :: from_str_unsafe ( "3.1.0" ) ,
402431 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" ) ,
405434 GenericRoleConfig :: default ( ) ,
406435 [
407436 (
0 commit comments