Skip to content

Commit 22198a2

Browse files
feat: Improve name types (#35)
* chore: Use specific types for each Kubernetes (resource) name * doc: Add code comments * doc: Add code comments * doc: Fix references * chore: Regenerate charts * fix: Require an RFC 1035 label name for Services * feat: Extend the compile-time checks for name types
1 parent f62cbd1 commit 22198a2

File tree

26 files changed

+1201
-383
lines changed

26 files changed

+1201
-383
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.nix

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ snafu = "0.8"
2323
strum = { version = "0.27", features = ["derive"] }
2424
tokio = { version = "1.45", features = ["full"] }
2525
tracing = "0.1"
26+
uuid = "1.18"
2627

2728
#[patch."https://github.com/stackabletech/operator-rs"]
2829
# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" }

deploy/helm/opensearch-operator/crds/crds.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ spec:
290290
type: object
291291
roleGroups:
292292
additionalProperties:
293+
description: Variant of [`stackable_operator::role_utils::GenericProductSpecificCommonConfig`] that implements [`Merge`]
293294
properties:
294295
cliOverrides:
295296
additionalProperties:

rust/operator-binary/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ snafu.workspace = true
2121
strum.workspace = true
2222
tokio.workspace = true
2323
tracing.workspace = true
24+
uuid.workspace = true
2425

2526
[build-dependencies]
2627
built.workspace = true

rust/operator-binary/src/controller.rs

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
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+
16
use std::{collections::BTreeMap, marker::PhantomData, str::FromStr, sync::Arc};
27

38
use 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;
3944
mod update_status;
4045
mod 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.
4251
pub struct ContextNames {
4352
pub product_name: ProductName,
4453
pub operator_name: OperatorName,
4554
pub controller_name: ControllerName,
4655
}
4756

57+
/// The controller context
4858
pub struct Context {
4959
client: stackable_operator::client::Client,
5060
names: ContextNames,
@@ -113,6 +123,7 @@ type OpenSearchRoleGroupConfig =
113123
type OpenSearchNodeResources =
114124
stackable_operator::commons::resources::Resources<v1alpha1::StorageConfig>;
115125

126+
/// The validated [`v1alpha1::OpenSearchConfig`]
116127
#[derive(Clone, Debug, PartialEq)]
117128
pub 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)]
129145
pub 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

207221
impl 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
262282
pub 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
305327
struct Prepared;
328+
/// Marker for applied Kubernetes resources
306329
struct 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.
308336
struct 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

Comments
 (0)