Skip to content

Commit 6f21e76

Browse files
maltesandersiegfriedweberadwk67
authored
refactor: Introduce build aggregator (#776)
* refactor: pass ServiceAccount name to StatefulSet builder * refactor: introduce the build aggregator * refactor: fold the discovery ConfigMap into the build aggregator * test: cover the build aggregator's resource set * fix: add role to error variants * changelog --------- Co-authored-by: Siegfried Weber <mail@siegfriedweber.net> Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech>
1 parent 7ab8839 commit 6f21e76

6 files changed

Lines changed: 255 additions & 134 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## [Unreleased]
44

5+
### Changed
6+
7+
- Internal operator refactoring: introduce a build() step in the reconciler that
8+
assembles all relevant Kubernetes resources before anything is applied ([#776]).
9+
10+
[#776]: https://github.com/stackabletech/hbase-operator/pull/776
11+
512
## [26.7.0] - 2026-07-21
613

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

rust/operator-binary/src/controller/build/mod.rs

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,119 @@
33
44
use std::str::FromStr;
55

6-
use stackable_operator::v2::types::operator::RoleGroupName;
6+
use snafu::{ResultExt, Snafu};
7+
use stackable_operator::{
8+
utils::cluster_info::KubernetesClusterInfo, v2::types::operator::RoleGroupName,
9+
};
10+
11+
use crate::{
12+
controller::{
13+
KubernetesResources, ValidatedCluster,
14+
build::resource::{
15+
config_map::{self, build_rolegroup_config_map},
16+
discovery::{self, build_discovery_config_map},
17+
pdb::build_pdb,
18+
service::{build_rolegroup_metrics_service, build_rolegroup_service},
19+
statefulset::{self, build_rolegroup_statefulset},
20+
},
21+
},
22+
crd::HbaseRole,
23+
};
724

825
// Placeholder role-group name used for the recommended labels of the role-level discovery
926
// `ConfigMap` (which is not tied to a single role group).
1027
stackable_operator::constant!(pub(crate) PLACEHOLDER_DISCOVERY_ROLE_GROUP: RoleGroupName = "discovery");
1128

29+
#[derive(Snafu, Debug)]
30+
pub enum Error {
31+
#[snafu(display("failed to build ConfigMap for role {hbase_role} role group {role_group}"))]
32+
ConfigMap {
33+
source: config_map::Error,
34+
hbase_role: HbaseRole,
35+
role_group: RoleGroupName,
36+
},
37+
38+
#[snafu(display("failed to build StatefulSet for role {hbase_role} role group {role_group}"))]
39+
StatefulSet {
40+
source: statefulset::Error,
41+
hbase_role: HbaseRole,
42+
role_group: RoleGroupName,
43+
},
44+
45+
#[snafu(display("failed to build discovery ConfigMap"))]
46+
Discovery { source: discovery::Error },
47+
}
48+
49+
/// Builds every Kubernetes resource for the given validated cluster.
50+
///
51+
/// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
52+
/// dereferenced and validated by this point, so the errors returned here are resource-assembly
53+
/// failures only. `cluster_info` is static cluster metadata (not a client call), and
54+
/// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under
55+
/// (RBAC resources are built and applied separately, in the reconcile step).
56+
pub fn build(
57+
cluster: &ValidatedCluster,
58+
cluster_info: &KubernetesClusterInfo,
59+
service_account_name: &str,
60+
) -> Result<KubernetesResources, Error> {
61+
let mut stateful_sets = vec![];
62+
let mut services = vec![];
63+
let mut config_maps = vec![];
64+
let mut pod_disruption_budgets = vec![];
65+
66+
for (hbase_role, role_group_configs) in &cluster.role_group_configs {
67+
for (role_group_name, rg_config) in role_group_configs {
68+
services.push(build_rolegroup_service(
69+
cluster,
70+
hbase_role,
71+
role_group_name,
72+
));
73+
services.push(build_rolegroup_metrics_service(
74+
cluster,
75+
hbase_role,
76+
role_group_name,
77+
));
78+
config_maps.push(
79+
build_rolegroup_config_map(cluster, cluster_info, hbase_role, role_group_name)
80+
.with_context(|_| ConfigMapSnafu {
81+
hbase_role: hbase_role.clone(),
82+
role_group: role_group_name.clone(),
83+
})?,
84+
);
85+
stateful_sets.push(
86+
build_rolegroup_statefulset(
87+
cluster,
88+
hbase_role,
89+
role_group_name,
90+
rg_config,
91+
service_account_name,
92+
)
93+
.with_context(|_| StatefulSetSnafu {
94+
hbase_role: hbase_role.clone(),
95+
role_group: role_group_name.clone(),
96+
})?,
97+
);
98+
}
99+
100+
if let Some(role_config) = cluster.role_configs.get(hbase_role)
101+
&& let Some(pdb) = build_pdb(&role_config.pdb, cluster, hbase_role)
102+
{
103+
pod_disruption_budgets.push(pdb);
104+
}
105+
}
106+
107+
// The role-level discovery ConfigMap advertises the cluster's connection information; it is
108+
// deterministic (derived only from the validated cluster and static cluster info).
109+
config_maps.push(build_discovery_config_map(cluster, cluster_info).context(DiscoverySnafu)?);
110+
111+
Ok(KubernetesResources {
112+
stateful_sets,
113+
services,
114+
config_maps,
115+
pod_disruption_budgets,
116+
})
117+
}
118+
12119
pub mod graceful_shutdown;
13120
pub mod jvm;
14121
pub mod kerberos;
@@ -17,3 +124,66 @@ pub mod properties;
17124
pub mod region_mover;
18125
pub mod resource;
19126
pub mod role;
127+
128+
#[cfg(test)]
129+
mod tests {
130+
use stackable_operator::kube::Resource;
131+
132+
use super::build;
133+
use crate::test_utils;
134+
135+
/// Collects the `.metadata.name`s of the given resources, sorted for stable comparison.
136+
fn sorted_names(resources: &[impl Resource]) -> Vec<&str> {
137+
let mut names: Vec<&str> = resources
138+
.iter()
139+
.filter_map(|resource| resource.meta().name.as_deref())
140+
.collect();
141+
names.sort();
142+
names
143+
}
144+
145+
#[test]
146+
fn build_produces_expected_resource_names() {
147+
let cluster = test_utils::validated_cluster();
148+
let cluster_info = test_utils::cluster_info();
149+
let resources =
150+
build(&cluster, &cluster_info, "hbase-serviceaccount").expect("build succeeds");
151+
152+
// One StatefulSet per role group (one `default` group for each of the three roles).
153+
assert_eq!(
154+
sorted_names(&resources.stateful_sets),
155+
[
156+
"hbase-master-default",
157+
"hbase-regionserver-default",
158+
"hbase-restserver-default",
159+
]
160+
);
161+
// One headless and one metrics Service per role group.
162+
assert_eq!(
163+
sorted_names(&resources.services),
164+
[
165+
"hbase-master-default-headless",
166+
"hbase-master-default-metrics",
167+
"hbase-regionserver-default-headless",
168+
"hbase-regionserver-default-metrics",
169+
"hbase-restserver-default-headless",
170+
"hbase-restserver-default-metrics",
171+
]
172+
);
173+
// One ConfigMap per role group plus the cluster-wide discovery ConfigMap (`hbase`).
174+
assert_eq!(
175+
sorted_names(&resources.config_maps),
176+
[
177+
"hbase",
178+
"hbase-master-default",
179+
"hbase-regionserver-default",
180+
"hbase-restserver-default",
181+
]
182+
);
183+
// A default PodDisruptionBudget per role.
184+
assert_eq!(
185+
sorted_names(&resources.pod_disruption_budgets),
186+
["hbase-master", "hbase-regionserver", "hbase-restserver"]
187+
);
188+
}
189+
}

rust/operator-binary/src/controller/build/resource/statefulset.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,10 @@ use stackable_operator::{
1515
DeepMerge,
1616
api::{
1717
apps::v1::{StatefulSet, StatefulSetSpec},
18-
core::v1::{
19-
ConfigMapVolumeSource, ContainerPort, Probe, ServiceAccount, TCPSocketAction,
20-
Volume,
21-
},
18+
core::v1::{ConfigMapVolumeSource, ContainerPort, Probe, TCPSocketAction, Volume},
2219
},
2320
apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
2421
},
25-
kube::ResourceExt,
2622
product_logging,
2723
v2::{
2824
builder::pod::container::{EnvVarName, EnvVarSet, new_container_builder},
@@ -110,7 +106,7 @@ pub fn build_rolegroup_statefulset(
110106
hbase_role: &HbaseRole,
111107
role_group_name: &RoleGroupName,
112108
validated_rg_config: &HbaseRoleGroupConfig,
113-
service_account: &ServiceAccount,
109+
service_account_name: &str,
114110
) -> Result<StatefulSet> {
115111
let resolved_product_image = &cluster.image;
116112
let merged_config = &validated_rg_config.config.config;
@@ -243,7 +239,7 @@ pub fn build_rolegroup_statefulset(
243239
)),
244240
)
245241
.context(AddVolumeSnafu)?
246-
.service_account_name(service_account.name_any())
242+
.service_account_name(service_account_name)
247243
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
248244

249245
// The HBase container's log config ConfigMap: either the operator-generated one (the

rust/operator-binary/src/controller/mod.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ pub use stackable_operator::v2::types::operator::RoleGroupName;
1010
use stackable_operator::{
1111
builder::meta::ObjectMetaBuilder,
1212
commons::product_image_selection::ResolvedProductImage,
13-
k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta,
13+
k8s_openapi::{
14+
api::{
15+
apps::v1::StatefulSet,
16+
core::v1::{ConfigMap, Service},
17+
policy::v1::PodDisruptionBudget,
18+
},
19+
apimachinery::pkg::apis::meta::v1::ObjectMeta,
20+
},
1421
kube::Resource,
1522
kvp::Labels,
1623
v2::{
@@ -51,6 +58,17 @@ pub(crate) fn controller_name() -> ControllerName {
5158
.expect("the controller name is a valid label value")
5259
}
5360

61+
/// The complete set of Kubernetes resources built for a [`ValidatedCluster`], ready to be applied.
62+
///
63+
/// hbase exposes its listeners as volume/PVC sources inside the `StatefulSet` rather than as
64+
/// top-level `Listener` objects, so (unlike some sibling operators) there is no `listeners` field.
65+
pub struct KubernetesResources {
66+
pub stateful_sets: Vec<StatefulSet>,
67+
pub services: Vec<Service>,
68+
pub config_maps: Vec<ConfigMap>,
69+
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
70+
}
71+
5472
/// The validated cluster: proves that config merging and validation succeeded for
5573
/// every role and role group before any resources are created.
5674
#[derive(Clone, Debug)]

0 commit comments

Comments
 (0)