Skip to content

Commit ea9aaee

Browse files
authored
refactor: Introduce role-builder (#833)
* refactor: role-builder * replace common role group builder with individual ones for each role * resolve role groups up front, then emit their resources * keep only the role on ContainerConfig::Hdfs * review feedback * review feedback: consolidate errors and use builder loops with a helper * changelog * changelog II
1 parent 369630e commit ea9aaee

12 files changed

Lines changed: 1043 additions & 964 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ All notable changes to this project will be documented in this file.
3636
- Internal operator refactoring: the validated cluster carries each role's configuration in typed
3737
per-role fields instead of maps keyed by role, so role-specific values are resolved once and the
3838
shared resource builders can no longer be handed another role's configuration ([#830]).
39+
- Internal operator refactoring: each role now has its own module that gathers its role group's
40+
inputs and role-specific containers, and a shared role-builder then builds the Services,
41+
ConfigMap and StatefulSet from them ([#833]).
3942
- Bump stackable-operator to 0.119.0 ([#835]).
4043

4144
### Fixed
@@ -63,6 +66,7 @@ All notable changes to this project will be documented in this file.
6366
[#829]: https://github.com/stackabletech/hdfs-operator/pull/829
6467
[#830]: https://github.com/stackabletech/hdfs-operator/pull/830
6568
[#831]: https://github.com/stackabletech/hdfs-operator/pull/831
69+
[#833]: https://github.com/stackabletech/hdfs-operator/pull/833
6670
[#835]: https://github.com/stackabletech/hdfs-operator/pull/835
6771

6872
## [26.7.0] - 2026-07-21

‎rust/operator-binary/src/controller/build/container.rs‎

Lines changed: 115 additions & 268 deletions
Large diffs are not rendered by default.

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

Lines changed: 68 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,19 @@ use stackable_operator::{
2323
},
2424
},
2525
};
26+
use strum::IntoEnumIterator;
2627

2728
use crate::{
2829
controller::{
2930
CONTROLLER_NAME, KubernetesResources, OPERATOR_NAME, PRODUCT_NAME, Prepared,
3031
ValidatedCluster,
31-
build::resource::rbac::{build_role_binding, build_service_account},
32+
build::{
33+
resource::rbac::{build_role_binding, build_service_account},
34+
role_group::{
35+
RoleGroupBuilder, build_datanode_role_group, build_journalnode_role_group,
36+
build_namenode_role_group,
37+
},
38+
},
3239
},
3340
crd::{
3441
HdfsNodeRole, HdfsPodRef,
@@ -57,61 +64,19 @@ pub mod jvm;
5764
pub mod kerberos;
5865
pub mod opa;
5966
pub mod properties;
60-
pub mod resolve;
6167
pub mod resource;
68+
pub mod role_group;
6269

6370
#[derive(Snafu, Debug)]
6471
pub enum Error {
65-
#[snafu(display("failed to build Service for role {role} role group {role_group}", role = role.as_ref()))]
66-
Service {
67-
source: resource::service::Error,
68-
role: HdfsNodeRole,
69-
role_group: RoleGroupName,
70-
},
71-
72-
#[snafu(display("failed to build ConfigMap for role {role} role group {role_group}", role = role.as_ref()))]
73-
ConfigMap {
74-
source: resource::config_map::Error,
75-
role: HdfsNodeRole,
76-
role_group: RoleGroupName,
77-
},
78-
79-
#[snafu(display("failed to build StatefulSet for role {role} role group {role_group}", role = role.as_ref()))]
80-
StatefulSet {
81-
source: resource::statefulset::Error,
82-
role: HdfsNodeRole,
83-
role_group: RoleGroupName,
84-
},
72+
#[snafu(display("failed to build the resources of a role group"))]
73+
RoleGroup { source: role_group::Error },
8574

8675
#[snafu(display("failed to build the discovery ConfigMap"))]
8776
DiscoveryConfigMap { source: resource::discovery::Error },
88-
89-
#[snafu(display("failed to build selector labels for role {role} role group {role_group}", role = role.as_ref()))]
90-
RoleGroupSelectorLabels {
91-
source: LabelError,
92-
role: HdfsNodeRole,
93-
role_group: RoleGroupName,
94-
},
95-
96-
#[snafu(display("failed to build volume claim templates for role {role} role group {role_group}", role = role.as_ref()))]
97-
VolumeClaimTemplates {
98-
source: container::Error,
99-
role: HdfsNodeRole,
100-
role_group: RoleGroupName,
101-
},
102-
103-
#[snafu(display("failed to build listener volume for role {role} role group {role_group}", role = role.as_ref()))]
104-
ListenerVolume {
105-
source: container::Error,
106-
role: HdfsNodeRole,
107-
role_group: RoleGroupName,
108-
},
10977
}
11078

111-
pub(crate) use resolve::RoleGroupResolver;
112-
pub use resolve::{ResolvedRoleGroup, RoleGroupLogging, RoleSpecificValues};
113-
114-
/// The resources of every role, accumulated one role at a time by [`build_role`].
79+
/// The resources of every role, accumulated one role group at a time by [`build`].
11580
#[derive(Default)]
11681
struct RoleGroupResources {
11782
services: Vec<Service>,
@@ -122,63 +87,18 @@ struct RoleGroupResources {
12287
pod_disruption_budgets: Vec<PodDisruptionBudget>,
12388
}
12489

125-
/// Builds every resource of every role group of one role, plus that role's PDB, appending them to
126-
/// `rg_resources`.
127-
fn build_role<C: RoleGroupResolver>(
128-
cluster: &ValidatedCluster,
129-
cluster_info: &KubernetesClusterInfo,
130-
role_group_configs: &BTreeMap<
131-
RoleGroupName,
132-
RoleGroupConfig<C, JavaCommonConfig, v1alpha1::HdfsConfigOverrides>,
133-
>,
134-
rg_resources: &mut RoleGroupResources,
135-
) -> Result<(), Error> {
136-
let role = &C::ROLE;
137-
138-
for (role_group_name, rg_config) in role_group_configs {
139-
build_role_group_services(cluster, role, role_group_name, &mut rg_resources.services)?;
140-
141-
let selector_labels = rolegroup_selector_labels(cluster, role, role_group_name).context(
142-
RoleGroupSelectorLabelsSnafu {
143-
role: *role,
144-
role_group: role_group_name.clone(),
145-
},
146-
)?;
147-
let resolved = rg_config.config.resolve(role_group_name, selector_labels)?;
148-
149-
rg_resources.config_maps.push(
150-
resource::config_map::build_rolegroup_config_map(
151-
cluster,
152-
cluster_info,
153-
role_group_name,
154-
rg_config,
155-
&resolved,
156-
)
157-
.context(ConfigMapSnafu {
158-
role: *role,
159-
role_group: role_group_name.clone(),
160-
})?,
161-
);
162-
rg_resources.stateful_sets.entry(C::ROLE).or_default().push(
163-
resource::statefulset::build_rolegroup_statefulset(
164-
cluster,
165-
cluster_info,
166-
role_group_name,
167-
rg_config,
168-
&resolved,
169-
)
170-
.context(StatefulSetSnafu {
171-
role: *role,
172-
role_group: role_group_name.clone(),
173-
})?,
174-
);
90+
impl RoleGroupResources {
91+
/// Builds the Services, ConfigMap and StatefulSet of one role group and adds them to the
92+
/// collections.
93+
fn add(&mut self, builder: &RoleGroupBuilder) -> Result<(), role_group::Error> {
94+
self.services.extend(builder.build_services()?);
95+
self.config_maps.push(builder.build_config_map()?);
96+
self.stateful_sets
97+
.entry(builder.role)
98+
.or_default()
99+
.push(builder.build_statefulset()?);
100+
Ok(())
175101
}
176-
177-
if let Some(pdb) = resource::pdb::build_pdb(cluster, role) {
178-
rg_resources.pod_disruption_budgets.push(pdb);
179-
}
180-
181-
Ok(())
182102
}
183103

184104
/// Builds every Kubernetes resource for the given validated cluster.
@@ -188,6 +108,11 @@ fn build_role<C: RoleGroupResolver>(
188108
/// `cluster_info` carries static cluster information resolved at operator startup (e.g. the
189109
/// cluster domain used to build Kerberos principals), not a live client.
190110
///
111+
/// Each of the three loops hands its role group's typed config to that role's builder, which is
112+
/// where everything specific to the role lives. The loops are free to be reordered: the
113+
/// StatefulSets are keyed by role, and the apply step does not depend on the order of the other
114+
/// three collections.
115+
///
191116
/// The resources are returned as flat collections. `stateful_sets` comes out in [`HdfsNodeRole`]
192117
/// order, which the apply step depends on; that is structural, from a [`BTreeMap`] flattened in
193118
/// key order, not from the order the roles are built in.
@@ -200,26 +125,33 @@ pub fn build(
200125
) -> Result<KubernetesResources<Prepared>, Error> {
201126
let mut built = RoleGroupResources::default();
202127

203-
// These three calls are free to be reordered: the StatefulSets are keyed by role, and the
204-
// apply step does not depend on the order of the other three collections.
205-
build_role(
206-
cluster,
207-
cluster_info,
208-
&cluster.journalnode_role_group_configs,
209-
&mut built,
210-
)?;
211-
build_role(
212-
cluster,
213-
cluster_info,
214-
&cluster.namenode_role_group_configs,
215-
&mut built,
216-
)?;
217-
build_role(
218-
cluster,
219-
cluster_info,
220-
&cluster.datanode_role_group_configs,
221-
&mut built,
222-
)?;
128+
for (role_group_name, rg_config) in &cluster.journalnode_role_group_configs {
129+
let builder =
130+
build_journalnode_role_group(cluster, cluster_info, role_group_name, rg_config)
131+
.context(RoleGroupSnafu)?;
132+
133+
built.add(&builder).context(RoleGroupSnafu)?;
134+
}
135+
136+
for (role_group_name, rg_config) in &cluster.namenode_role_group_configs {
137+
let builder = build_namenode_role_group(cluster, cluster_info, role_group_name, rg_config)
138+
.context(RoleGroupSnafu)?;
139+
140+
built.add(&builder).context(RoleGroupSnafu)?;
141+
}
142+
143+
for (role_group_name, rg_config) in &cluster.datanode_role_group_configs {
144+
let builder = build_datanode_role_group(cluster, cluster_info, role_group_name, rg_config)
145+
.context(RoleGroupSnafu)?;
146+
147+
built.add(&builder).context(RoleGroupSnafu)?;
148+
}
149+
150+
for role in HdfsNodeRole::iter() {
151+
if let Some(pdb) = resource::pdb::build_pdb(cluster, &role) {
152+
built.pod_disruption_budgets.push(pdb);
153+
}
154+
}
223155

224156
let RoleGroupResources {
225157
services,
@@ -250,34 +182,6 @@ pub fn build(
250182
})
251183
}
252184

253-
/// Builds the two Services for one role group. Role-agnostic: it reads nothing from the role
254-
/// config.
255-
fn build_role_group_services(
256-
cluster: &ValidatedCluster,
257-
role: &HdfsNodeRole,
258-
role_group_name: &RoleGroupName,
259-
services: &mut Vec<Service>,
260-
) -> Result<(), Error> {
261-
services.push(
262-
resource::service::rolegroup_headless_service(cluster, role, role_group_name).context(
263-
ServiceSnafu {
264-
role: *role,
265-
role_group: role_group_name.clone(),
266-
},
267-
)?,
268-
);
269-
services.push(
270-
resource::service::rolegroup_metrics_service(cluster, role, role_group_name).context(
271-
ServiceSnafu {
272-
role: *role,
273-
role_group: role_group_name.clone(),
274-
},
275-
)?,
276-
);
277-
278-
Ok(())
279-
}
280-
281185
/// The replica count a role group gets when it does not set one: Kubernetes runs a single pod for
282186
/// a `StatefulSet` with `replicas: null`.
283187
pub(crate) const DEFAULT_REPLICAS: u16 = 1;
@@ -456,8 +360,19 @@ pub(crate) fn native_metrics_port(cluster: &ValidatedCluster, role: &HdfsNodeRol
456360
}
457361
}
458362

459-
/// The deprecated JMX exporter metrics port for the given `role`.
460-
fn jmx_metrics_port(role: &HdfsNodeRole) -> Port {
363+
/// The name of the port the given `role` serves IPC/RPC on, which its readiness probe checks.
364+
///
365+
/// The datanodes call theirs `ipc`, the other two `rpc`; the same names [`role_data_ports`]
366+
/// exposes them under.
367+
pub(crate) fn ipc_port_name(role: &HdfsNodeRole) -> &'static str {
368+
match role {
369+
HdfsNodeRole::Name | HdfsNodeRole::Journal => SERVICE_PORT_NAME_RPC,
370+
HdfsNodeRole::Data => SERVICE_PORT_NAME_IPC,
371+
}
372+
}
373+
374+
/// The deprecated JMX Exporter metrics port for the given `role`.
375+
pub(crate) fn jmx_metrics_port(role: &HdfsNodeRole) -> Port {
461376
match role {
462377
HdfsNodeRole::Name => DEFAULT_NAME_NODE_METRICS_PORT,
463378
HdfsNodeRole::Data => DEFAULT_DATA_NODE_METRICS_PORT,

0 commit comments

Comments
 (0)