Skip to content

Commit ade8a87

Browse files
maltesanderadwk67siegfriedweber
authored
refactor: Introduce build aggregator (#726)
* refactor: Introduce build aggregator * refactor: Drop obsolete enum_variant_names allow * Apply suggestions from code review Co-authored-by: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> * changelog --------- Co-authored-by: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> Co-authored-by: Siegfried Weber <mail@siegfriedweber.net> Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech>
1 parent 8c2678d commit ade8a87

3 files changed

Lines changed: 249 additions & 120 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 ([#726]).
11+
12+
[#726]: https://github.com/stackabletech/hive-operator/pull/726
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: 80 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ use stackable_operator::{
2121
},
2222
crd::{listener::v1alpha1::Listener, s3},
2323
database_connections::drivers::jdbc::JdbcDatabaseConnectionDetails,
24+
k8s_openapi::api::{
25+
apps::v1::StatefulSet,
26+
core::v1::{ConfigMap, Service},
27+
policy::v1::PodDisruptionBudget,
28+
},
2429
kube::{
2530
Resource, ResourceExt,
2631
api::ObjectMeta,
@@ -49,12 +54,7 @@ use strum::EnumDiscriminants;
4954

5055
use crate::{
5156
OPERATOR_NAME,
52-
controller::build::resource::{
53-
discovery,
54-
listener::build_role_listener,
55-
pdb::build_pdb,
56-
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
57-
},
57+
controller::build::resource::discovery,
5858
crd::{APP_NAME, HdfsConnection, HiveClusterStatus, HiveRole, MetaStoreConfig, v1alpha1},
5959
};
6060

@@ -68,30 +68,13 @@ pub struct Ctx {
6868

6969
#[derive(Snafu, Debug, EnumDiscriminants)]
7070
#[strum_discriminants(derive(strum::IntoStaticStr))]
71-
#[allow(clippy::enum_variant_names)]
7271
pub enum Error {
73-
#[snafu(display("failed to apply Service for role group {role_group}"))]
74-
ApplyRoleGroupService {
75-
source: stackable_operator::cluster_resources::Error,
76-
role_group: RoleGroupName,
77-
},
78-
79-
#[snafu(display("failed to build ConfigMap for role group {role_group}"))]
80-
BuildRoleGroupConfigMap {
81-
source: build::resource::config_map::Error,
82-
role_group: RoleGroupName,
83-
},
72+
#[snafu(display("failed to build the Kubernetes resources"))]
73+
BuildResources { source: build::Error },
8474

85-
#[snafu(display("failed to apply ConfigMap for role group {role_group}"))]
86-
ApplyRoleGroupConfig {
75+
#[snafu(display("failed to apply Kubernetes resource"))]
76+
ApplyResource {
8777
source: stackable_operator::cluster_resources::Error,
88-
role_group: RoleGroupName,
89-
},
90-
91-
#[snafu(display("failed to apply StatefulSet for role group {role_group}"))]
92-
ApplyRoleGroupStatefulSet {
93-
source: stackable_operator::cluster_resources::Error,
94-
role_group: RoleGroupName,
9578
},
9679

9780
#[snafu(display("failed to build discovery ConfigMap"))]
@@ -127,11 +110,6 @@ pub enum Error {
127110
source: stackable_operator::commons::rbac::Error,
128111
},
129112

130-
#[snafu(display("failed to apply PodDisruptionBudget"))]
131-
ApplyPdb {
132-
source: stackable_operator::cluster_resources::Error,
133-
},
134-
135113
#[snafu(display("failed to get required Labels"))]
136114
GetRequiredLabels {
137115
source:
@@ -143,24 +121,13 @@ pub enum Error {
143121
source: error_boundary::InvalidObject,
144122
},
145123

146-
#[snafu(display("failed to apply group listener for {role}"))]
147-
ApplyGroupListener {
148-
source: stackable_operator::cluster_resources::Error,
149-
role: String,
150-
},
151124
#[snafu(display("failed to dereference cluster resources"))]
152125
Dereference {
153126
source: crate::controller::dereference::Error,
154127
},
155128

156129
#[snafu(display("failed to validate cluster configuration"))]
157130
Validate { source: validate::Error },
158-
159-
#[snafu(display("failed to build StatefulSet for role group {role_group}"))]
160-
BuildRoleGroupStatefulSet {
161-
source: build::resource::statefulset::Error,
162-
role_group: RoleGroupName,
163-
},
164131
}
165132
type Result<T, E = Error> = std::result::Result<T, E>;
166133

@@ -462,6 +429,19 @@ pub struct ValidatedRoleConfig {
462429
pub listener_class: ListenerClassName,
463430
}
464431

432+
/// Every Kubernetes resource produced by the client-free [`build`](build::build) step.
433+
///
434+
/// The role-level discovery `ConfigMap` is deliberately absent: it is built from the *applied*
435+
/// role [`Listener`]'s ingress addresses, so it is assembled in the reconcile step after the
436+
/// Listener has been applied, not in the build step.
437+
pub struct KubernetesResources {
438+
pub stateful_sets: Vec<StatefulSet>,
439+
pub services: Vec<Service>,
440+
pub listeners: Vec<Listener>,
441+
pub config_maps: Vec<ConfigMap>,
442+
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
443+
}
444+
465445
pub async fn reconcile_hive(
466446
hive: Arc<DeserializeGuard<v1alpha1::HiveCluster>>,
467447
ctx: Arc<Ctx>,
@@ -515,98 +495,79 @@ pub async fn reconcile_hive(
515495
.await
516496
.context(ApplyRoleBindingSnafu)?;
517497

518-
let mut ss_cond_builder = StatefulSetConditionBuilder::default();
498+
// The ServiceAccount name is deterministic on the built object, so the client-free build step
499+
// does not depend on the applied ServiceAccount.
500+
let service_account_name = rbac_sa.name_any();
519501

520-
for (hive_role, role_group_configs) in &validated_cluster.role_group_configs {
521-
for (role_group_name, rg) in role_group_configs {
522-
let rg_metrics_service =
523-
build_rolegroup_metrics_service(&validated_cluster, role_group_name);
502+
let resources = build::build(
503+
&validated_cluster,
504+
&client.kubernetes_cluster_info,
505+
&service_account_name,
506+
)
507+
.context(BuildResourcesSnafu)?;
524508

525-
let rg_headless_service =
526-
build_rolegroup_headless_service(&validated_cluster, role_group_name);
509+
let mut ss_cond_builder = StatefulSetConditionBuilder::default();
527510

528-
let rg_configmap = build::resource::config_map::build_metastore_rolegroup_config_map(
529-
&validated_cluster,
530-
&client.kubernetes_cluster_info,
531-
role_group_name,
532-
rg,
533-
)
534-
.with_context(|_| BuildRoleGroupConfigMapSnafu {
535-
role_group: role_group_name.clone(),
536-
})?;
537-
538-
let rg_statefulset =
539-
build::resource::statefulset::build_metastore_rolegroup_statefulset(
540-
hive_role,
541-
&validated_cluster,
542-
role_group_name,
543-
rg,
544-
&rbac_sa.name_any(),
545-
)
546-
.with_context(|_| BuildRoleGroupStatefulSetSnafu {
547-
role_group: role_group_name.clone(),
548-
})?;
511+
// Apply order: everything before StatefulSets, StatefulSets last. A StatefulSet must only be
512+
// applied after all ConfigMaps and Secrets it mounts, to prevent unnecessary Pod restarts.
513+
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
514+
for service in resources.services {
515+
cluster_resources
516+
.add(client, service)
517+
.await
518+
.context(ApplyResourceSnafu)?;
519+
}
549520

521+
// The role Listener is applied before the discovery ConfigMap, which is built below from the
522+
// applied Listener's ingress addresses. Hive has a single role Listener, so at most one is
523+
// captured here.
524+
let mut applied_role_listener: Option<Listener> = None;
525+
for listener in resources.listeners {
526+
applied_role_listener = Some(
550527
cluster_resources
551-
.add(client, rg_metrics_service)
528+
.add(client, listener)
552529
.await
553-
.context(ApplyRoleGroupServiceSnafu {
554-
role_group: role_group_name.clone(),
555-
})?;
530+
.context(ApplyResourceSnafu)?,
531+
);
532+
}
533+
534+
for config_map in resources.config_maps {
535+
cluster_resources
536+
.add(client, config_map)
537+
.await
538+
.context(ApplyResourceSnafu)?;
539+
}
556540

541+
for pdb in resources.pod_disruption_budgets {
542+
cluster_resources
543+
.add(client, pdb)
544+
.await
545+
.context(ApplyResourceSnafu)?;
546+
}
547+
548+
for statefulset in resources.stateful_sets {
549+
ss_cond_builder.add(
557550
cluster_resources
558-
.add(client, rg_headless_service)
551+
.add(client, statefulset)
559552
.await
560-
.context(ApplyRoleGroupServiceSnafu {
561-
role_group: role_group_name.clone(),
562-
})?;
563-
564-
cluster_resources.add(client, rg_configmap).await.context(
565-
ApplyRoleGroupConfigSnafu {
566-
role_group: role_group_name.clone(),
567-
},
568-
)?;
569-
570-
// Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it
571-
// mounts to prevent unnecessary Pod restarts.
572-
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
573-
ss_cond_builder.add(
574-
cluster_resources
575-
.add(client, rg_statefulset)
576-
.await
577-
.context(ApplyRoleGroupStatefulSetSnafu {
578-
role_group: role_group_name.clone(),
579-
})?,
580-
);
581-
}
553+
.context(ApplyResourceSnafu)?,
554+
);
582555
}
583556

557+
// The discovery ConfigMap is built from the *applied* role Listener's ingress addresses, so it
558+
// is assembled here rather than in the client-free build step. Its applied resource version
559+
// feeds the status discovery hash.
584560
// std's SipHasher is deprecated, and DefaultHasher is unstable across Rust releases.
585561
// We don't /need/ stability, but it's still nice to avoid spurious changes where possible.
586562
let mut discovery_hash = FnvHasher::with_key(0);
587563

588-
if let Some(role_config) = &validated_cluster.role_config {
589-
if let Some(pdb) = build_pdb(&role_config.pdb, &validated_cluster, &HiveRole::MetaStore) {
590-
cluster_resources
591-
.add(client, pdb)
592-
.await
593-
.context(ApplyPdbSnafu)?;
594-
}
595-
596-
let role_listener: Listener = build_role_listener(
564+
if let Some(role_listener) = applied_role_listener {
565+
let discovery_cm = discovery::build_discovery_configmap(
597566
&validated_cluster,
598-
&HiveRole::MetaStore,
599-
&role_config.listener_class,
600-
);
601-
let listener = cluster_resources.add(client, role_listener).await.context(
602-
ApplyGroupListenerSnafu {
603-
role: HiveRole::MetaStore.to_string(),
604-
},
605-
)?;
606-
607-
let discovery_cm =
608-
discovery::build_discovery_configmap(&validated_cluster, HiveRole::MetaStore, listener)
609-
.context(BuildDiscoveryConfigSnafu)?;
567+
HiveRole::MetaStore,
568+
role_listener,
569+
)
570+
.context(BuildDiscoveryConfigSnafu)?;
610571
let discovery_cm = cluster_resources
611572
.add(client, discovery_cm)
612573
.await

0 commit comments

Comments
 (0)