Skip to content

Commit 1fc71f1

Browse files
maltesandersiegfriedweberadwk67
authored
refactor: Introduce build aggregator (#841)
* chore: use service account name instead of resource * refactor: introduce KubernetesResource struct * chore: remove obsolete errors * changelog --------- Co-authored-by: Siegfried Weber <mail@siegfriedweber.net> Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech>
1 parent bb2ed0f commit 1fc71f1

4 files changed

Lines changed: 269 additions & 124 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Changed
8+
9+
- Internal operator refactoring: introduce a build() step in the reconciler that
10+
assembles all relevant Kubernetes resources before anything is applied ([#841]).
11+
12+
[#841]: https://github.com/stackabletech/druid-operator/pull/841
13+
714
## [26.7.0] - 2026-07-21
815

916
## [26.7.0-rc1] - 2026-07-16

rust/operator-binary/src/controller.rs

Lines changed: 92 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ use stackable_operator::{
99
cli::OperatorEnvironmentOptions,
1010
cluster_resources::ClusterResourceApplyStrategy,
1111
commons::rbac::build_rbac_resources,
12+
crd::listener::v1alpha1::Listener,
13+
k8s_openapi::api::{
14+
apps::v1::StatefulSet,
15+
core::v1::{ConfigMap, Service},
16+
policy::v1::PodDisruptionBudget,
17+
},
1218
kube::{
19+
ResourceExt,
1320
core::{DeserializeGuard, error_boundary},
1421
runtime::controller::Action,
1522
},
@@ -28,11 +35,7 @@ use stackable_operator::{
2835
use strum::{EnumDiscriminants, IntoStaticStr};
2936

3037
use crate::{
31-
controller::build::resource::{
32-
listener::{build_group_listener, group_listener_name},
33-
pdb::build_pdb,
34-
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
35-
},
38+
controller::build::resource::listener::{build_group_listener, group_listener_name},
3639
crd::{APP_NAME, DruidClusterStatus, DruidRole, OPERATOR_NAME, v1alpha1},
3740
internal_secret::create_shared_internal_secret,
3841
};
@@ -69,30 +72,28 @@ pub struct Ctx {
6972
pub operator_environment: OperatorEnvironmentOptions,
7073
}
7174

75+
/// Every Kubernetes resource produced by the client-free [`build`](build::build) step.
76+
///
77+
/// The Router group `Listener` and the discovery `ConfigMap`s are intentionally *not* yet part of this
78+
/// bundle: the discovery `ConfigMap` derives from the *applied* Router listener's ingress address,
79+
/// so both are built and applied in the reconcile step instead.
80+
pub struct KubernetesResources {
81+
pub stateful_sets: Vec<StatefulSet>,
82+
pub services: Vec<Service>,
83+
pub listeners: Vec<Listener>,
84+
pub config_maps: Vec<ConfigMap>,
85+
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
86+
}
87+
7288
#[derive(Snafu, Debug, EnumDiscriminants)]
7389
#[strum_discriminants(derive(IntoStaticStr))]
7490
pub enum Error {
75-
#[snafu(display("failed to apply Service for role group {role_group}"))]
76-
ApplyRoleGroupService {
77-
source: stackable_operator::cluster_resources::Error,
78-
role_group: String,
79-
},
80-
81-
#[snafu(display("failed to apply ConfigMap for role group {role_group}"))]
82-
ApplyRoleGroupConfig {
83-
source: stackable_operator::cluster_resources::Error,
84-
role_group: String,
85-
},
86-
87-
#[snafu(display("failed to build StatefulSet"))]
88-
BuildRoleGroupStatefulSet {
89-
source: build::resource::statefulset::Error,
90-
},
91+
#[snafu(display("failed to build the Kubernetes resources"))]
92+
BuildResources { source: build::Error },
9193

92-
#[snafu(display("failed to apply StatefulSet for role group {role_group}"))]
93-
ApplyRoleGroupStatefulSet {
94+
#[snafu(display("failed to apply Kubernetes resource"))]
95+
ApplyResource {
9496
source: stackable_operator::cluster_resources::Error,
95-
role_group: String,
9697
},
9798

9899
#[snafu(display("failed to dereference cluster objects"))]
@@ -136,11 +137,6 @@ pub enum Error {
136137
source: stackable_operator::commons::rbac::Error,
137138
},
138139

139-
#[snafu(display("failed to apply PodDisruptionBudget"))]
140-
ApplyPdb {
141-
source: stackable_operator::cluster_resources::Error,
142-
},
143-
144140
#[snafu(display("failed to get required labels"))]
145141
GetRequiredLabels {
146142
source: KeyValuePairError<LabelValueError>,
@@ -158,11 +154,6 @@ pub enum Error {
158154

159155
#[snafu(display("failed to validate cluster"))]
160156
ValidateCluster { source: validate::Error },
161-
162-
#[snafu(display("failed to build rolegroup ConfigMap"))]
163-
BuildConfigMap {
164-
source: build::resource::config_map::Error,
165-
},
166157
}
167158

168159
type Result<T, E = Error> = std::result::Result<T, E>;
@@ -213,9 +204,13 @@ pub async fn reconcile_druid(
213204
.context(GetRequiredLabelsSnafu)?,
214205
)
215206
.context(BuildRbacResourcesSnafu)?;
207+
208+
// The ServiceAccount name is deterministic on the built object, so the StatefulSet builder only
209+
// needs the name and does not depend on the applied ServiceAccount.
210+
let service_account_name = rbac_sa.name_any();
211+
216212
cluster_resources
217-
// We clone rbac_sa because we need to reuse it below
218-
.add(client, rbac_sa.clone())
213+
.add(client, rbac_sa)
219214
.await
220215
.context(ApplyServiceAccountSnafu)?;
221216
cluster_resources
@@ -229,99 +224,76 @@ pub async fn reconcile_druid(
229224
.await
230225
.context(FailedInternalSecretCreationSnafu)?;
231226

232-
let mut ss_cond_builder = StatefulSetConditionBuilder::default();
227+
let resources =
228+
build::build(&validated_cluster, &service_account_name).context(BuildResourcesSnafu)?;
233229

234-
for (druid_role, groups) in validated_cluster.role_group_configs.iter() {
235-
for (rolegroup_name, rg) in groups.iter() {
236-
let rg_headless_service =
237-
build_rolegroup_headless_service(&validated_cluster, druid_role, rolegroup_name);
238-
let rg_metrics_service =
239-
build_rolegroup_metrics_service(&validated_cluster, druid_role, rolegroup_name);
240-
241-
let rg_configmap = build::resource::config_map::build_rolegroup_config_map(
242-
&validated_cluster,
243-
druid_role,
244-
rolegroup_name,
245-
rg,
246-
)
247-
.context(BuildConfigMapSnafu)?;
248-
let rg_statefulset = build::resource::statefulset::build_rolegroup_statefulset(
249-
&validated_cluster,
250-
druid_role,
251-
rolegroup_name,
252-
rg,
253-
&rbac_sa,
254-
)
255-
.context(BuildRoleGroupStatefulSetSnafu)?;
230+
let mut ss_cond_builder = StatefulSetConditionBuilder::default();
256231

232+
// Apply order: everything a Pod mounts (ConfigMaps) must exist before the StatefulSets, so the
233+
// StatefulSets are applied last to prevent unnecessary Pod restarts.
234+
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
235+
for service in resources.services {
236+
cluster_resources
237+
.add(client, service)
238+
.await
239+
.context(ApplyResourceSnafu)?;
240+
}
241+
for listener in resources.listeners {
242+
cluster_resources
243+
.add(client, listener)
244+
.await
245+
.context(ApplyResourceSnafu)?;
246+
}
247+
for config_map in resources.config_maps {
248+
cluster_resources
249+
.add(client, config_map)
250+
.await
251+
.context(ApplyResourceSnafu)?;
252+
}
253+
for pdb in resources.pod_disruption_budgets {
254+
cluster_resources
255+
.add(client, pdb)
256+
.await
257+
.context(ApplyResourceSnafu)?;
258+
}
259+
for stateful_set in resources.stateful_sets {
260+
ss_cond_builder.add(
257261
cluster_resources
258-
.add(client, rg_headless_service)
259-
.await
260-
.with_context(|_| ApplyRoleGroupServiceSnafu {
261-
role_group: rolegroup_name.to_string(),
262-
})?;
263-
cluster_resources
264-
.add(client, rg_metrics_service)
265-
.await
266-
.with_context(|_| ApplyRoleGroupServiceSnafu {
267-
role_group: rolegroup_name.to_string(),
268-
})?;
269-
cluster_resources
270-
.add(client, rg_configmap)
262+
.add(client, stateful_set)
271263
.await
272-
.with_context(|_| ApplyRoleGroupConfigSnafu {
273-
role_group: rolegroup_name.to_string(),
274-
})?;
275-
276-
// Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts
277-
// to prevent unnecessary Pod restarts.
278-
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
279-
ss_cond_builder.add(
280-
cluster_resources
281-
.add(client, rg_statefulset)
282-
.await
283-
.with_context(|_| ApplyRoleGroupStatefulSetSnafu {
284-
role_group: rolegroup_name.to_string(),
285-
})?,
286-
);
287-
}
264+
.context(ApplyResourceSnafu)?,
265+
);
266+
}
288267

289-
if let Some(listener_class) = &validated_cluster.role_config(druid_role).listener_class
290-
&& let Some(listener_group_name) = group_listener_name(&validated_cluster, druid_role)
291-
{
292-
let role_group_listener = build_group_listener(
293-
&validated_cluster,
294-
listener_class,
295-
listener_group_name,
296-
druid_role,
297-
);
298-
299-
let listener = cluster_resources
300-
.add(client, role_group_listener)
301-
.await
302-
.context(ApplyGroupListenerSnafu)?;
303-
304-
if *druid_role == DruidRole::Router {
305-
// discovery
306-
for discovery_cm in build_discovery_configmaps(&validated_cluster, listener)
307-
.await
308-
.context(BuildDiscoveryConfigSnafu)?
309-
{
310-
cluster_resources
311-
.add(client, discovery_cm)
312-
.await
313-
.context(ApplyDiscoveryConfigSnafu)?;
314-
}
315-
}
316-
}
268+
// The Router group Listener and its discovery ConfigMaps are applied here rather than in the
269+
// build step: the discovery ConfigMap derives from the *applied* Router listener's ingress
270+
// address, which is only known after the Listener has been applied.
271+
if let Some(listener_class) = &validated_cluster
272+
.role_config(&DruidRole::Router)
273+
.listener_class
274+
&& let Some(listener_group_name) =
275+
group_listener_name(&validated_cluster, &DruidRole::Router)
276+
{
277+
let router_listener = build_group_listener(
278+
&validated_cluster,
279+
listener_class,
280+
listener_group_name,
281+
&DruidRole::Router,
282+
);
317283

318-
let role_config = validated_cluster.role_config(druid_role);
284+
let listener = cluster_resources
285+
.add(client, router_listener)
286+
.await
287+
.context(ApplyGroupListenerSnafu)?;
319288

320-
if let Some(pdb) = build_pdb(&role_config.pdb, &validated_cluster, druid_role) {
289+
for discovery_cm in build_discovery_configmaps(&validated_cluster, listener)
290+
.await
291+
.context(BuildDiscoveryConfigSnafu)?
292+
{
321293
cluster_resources
322-
.add(client, pdb)
294+
.add(client, discovery_cm)
323295
.await
324-
.context(ApplyPdbSnafu)?;
296+
.context(ApplyDiscoveryConfigSnafu)?;
325297
}
326298
}
327299

0 commit comments

Comments
 (0)