Skip to content

Commit 8af09ec

Browse files
committed
refactor: Introduce build aggregator
1 parent e812aba commit 8af09ec

3 files changed

Lines changed: 252 additions & 116 deletions

File tree

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

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::{collections::HashMap, str::FromStr};
22

3+
use snafu::{ResultExt, Snafu};
34
use stackable_operator::{
45
builder::meta::ObjectMetaBuilder,
56
kvp::{LabelError, Labels},
7+
utils::cluster_info::KubernetesClusterInfo,
68
v2::{
79
builder::meta::ownerreference_from_resource,
810
types::{common::Port, kubernetes::ServiceName, operator::RoleGroupName},
@@ -11,7 +13,7 @@ use stackable_operator::{
1113

1214
use crate::{
1315
build_recommended_labels,
14-
controller::ValidatedCluster,
16+
controller::{KubernetesResources, ValidatedCluster},
1517
crd::{
1618
HdfsNodeRole, HdfsPodRef,
1719
constants::{
@@ -41,6 +43,111 @@ pub mod opa;
4143
pub mod properties;
4244
pub mod resource;
4345

46+
#[derive(Snafu, Debug)]
47+
pub enum Error {
48+
#[snafu(display("failed to build Service for role {role} role group {role_group}"))]
49+
Service {
50+
source: resource::service::Error,
51+
role: HdfsNodeRole,
52+
role_group: RoleGroupName,
53+
},
54+
55+
#[snafu(display("failed to build ConfigMap for role {role} role group {role_group}"))]
56+
ConfigMap {
57+
source: resource::config_map::Error,
58+
role: HdfsNodeRole,
59+
role_group: RoleGroupName,
60+
},
61+
62+
#[snafu(display("failed to build StatefulSet for role {role} role group {role_group}"))]
63+
StatefulSet {
64+
source: resource::statefulset::Error,
65+
role: HdfsNodeRole,
66+
role_group: RoleGroupName,
67+
},
68+
}
69+
70+
/// Builds every Kubernetes resource for the given validated cluster.
71+
///
72+
/// Does not need a Kubernetes client: every external reference is already dereferenced and
73+
/// validated by this point, so the errors returned here are resource-assembly failures only.
74+
/// `cluster_info` carries static cluster information resolved at operator startup (e.g. the
75+
/// cluster domain used to build Kerberos principals), not a live client.
76+
///
77+
/// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under.
78+
/// The RBAC resources are built and applied separately in the reconcile step.
79+
///
80+
/// The resources are returned as flat, unordered collections. The reconcile step re-groups the
81+
/// StatefulSets by role to preserve HDFS's ordered, rollout-gated deployment during upgrades. The
82+
/// discovery `ConfigMap` is deliberately not built here: it needs a live client to resolve
83+
/// listener addresses and is therefore handled in the reconcile step.
84+
pub fn build(
85+
cluster: &ValidatedCluster,
86+
cluster_info: &KubernetesClusterInfo,
87+
service_account_name: &str,
88+
) -> Result<KubernetesResources, Error> {
89+
let mut services = vec![];
90+
let mut config_maps = vec![];
91+
let mut stateful_sets = vec![];
92+
let mut pod_disruption_budgets = vec![];
93+
94+
for (role, role_group_configs) in &cluster.role_groups {
95+
for (role_group_name, rg_config) in role_group_configs {
96+
services.push(
97+
resource::service::rolegroup_headless_service(cluster, role, role_group_name)
98+
.context(ServiceSnafu {
99+
role: *role,
100+
role_group: role_group_name.clone(),
101+
})?,
102+
);
103+
services.push(
104+
resource::service::rolegroup_metrics_service(cluster, role, role_group_name)
105+
.context(ServiceSnafu {
106+
role: *role,
107+
role_group: role_group_name.clone(),
108+
})?,
109+
);
110+
config_maps.push(
111+
resource::config_map::build_rolegroup_config_map(
112+
cluster,
113+
cluster_info,
114+
role,
115+
role_group_name,
116+
)
117+
.context(ConfigMapSnafu {
118+
role: *role,
119+
role_group: role_group_name.clone(),
120+
})?,
121+
);
122+
stateful_sets.push(
123+
resource::statefulset::build_rolegroup_statefulset(
124+
cluster,
125+
cluster_info,
126+
role,
127+
role_group_name,
128+
rg_config,
129+
service_account_name,
130+
)
131+
.context(StatefulSetSnafu {
132+
role: *role,
133+
role_group: role_group_name.clone(),
134+
})?,
135+
);
136+
}
137+
138+
if let Some(pdb) = resource::pdb::build_pdb(cluster, role) {
139+
pod_disruption_budgets.push(pdb);
140+
}
141+
}
142+
143+
Ok(KubernetesResources {
144+
services,
145+
config_maps,
146+
pod_disruption_budgets,
147+
stateful_sets,
148+
})
149+
}
150+
44151
/// Builds the [`HdfsPodRef`]s expected for every pod of the given `role`, across all
45152
/// of its role groups.
46153
///
@@ -263,3 +370,55 @@ fn role_data_ports(role: &HdfsNodeRole, https_enabled: bool) -> Vec<(String, Por
263370
],
264371
}
265372
}
373+
374+
#[cfg(test)]
375+
mod tests {
376+
use stackable_operator::kube::Resource;
377+
378+
use super::build;
379+
use crate::controller::build::properties::test_support::{cluster_info, validated_cluster};
380+
381+
/// The sorted `metadata.name`s of a resource collection.
382+
fn sorted_names(resources: &[impl Resource]) -> Vec<String> {
383+
let mut names: Vec<String> = resources
384+
.iter()
385+
.filter_map(|resource| resource.meta().name.clone())
386+
.collect();
387+
names.sort();
388+
names
389+
}
390+
391+
/// The aggregator emits, for the minimal three-role cluster (one `default` role group each):
392+
/// one StatefulSet and one ConfigMap per role group, one headless plus one metrics Service per
393+
/// role group, and one default PDB per role.
394+
#[test]
395+
fn build_produces_expected_resource_names() {
396+
let cluster = validated_cluster();
397+
let resources =
398+
build(&cluster, &cluster_info(), "hdfs-serviceaccount").expect("build succeeds");
399+
400+
assert_eq!(
401+
sorted_names(&resources.stateful_sets),
402+
[
403+
"hdfs-datanode-default",
404+
"hdfs-journalnode-default",
405+
"hdfs-namenode-default",
406+
]
407+
);
408+
// One headless and one metrics Service per role group.
409+
assert_eq!(resources.services.len(), 6);
410+
assert_eq!(
411+
sorted_names(&resources.config_maps),
412+
[
413+
"hdfs-datanode-default",
414+
"hdfs-journalnode-default",
415+
"hdfs-namenode-default",
416+
]
417+
);
418+
// A default PDB per role.
419+
assert_eq!(
420+
sorted_names(&resources.pod_disruption_budgets),
421+
["hdfs-datanode", "hdfs-journalnode", "hdfs-namenode"]
422+
);
423+
}
424+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ use std::{collections::BTreeMap, str::FromStr};
22

33
use stackable_operator::{
44
commons::product_image_selection::ResolvedProductImage,
5+
k8s_openapi::api::{
6+
apps::v1::StatefulSet,
7+
core::v1::{ConfigMap, Service},
8+
policy::v1::PodDisruptionBudget,
9+
},
510
kube::{Resource, api::ObjectMeta},
611
v2::{
712
HasName, HasUid, NameIsValidLabelValue,
@@ -30,6 +35,20 @@ pub mod build;
3035
pub mod dereference;
3136
pub mod validate;
3237

38+
/// Every Kubernetes resource produced by the build step.
39+
///
40+
/// The resources are flat, unordered collections. The reconcile step re-groups the
41+
/// StatefulSets by role to preserve HDFS's ordered, rollout-gated deployment during
42+
/// upgrades. The discovery `ConfigMap` is not part of this set: it depends on a live
43+
/// Kubernetes client (to resolve listener addresses) and is therefore built and applied
44+
/// separately in the reconcile step.
45+
pub struct KubernetesResources {
46+
pub services: Vec<Service>,
47+
pub config_maps: Vec<ConfigMap>,
48+
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
49+
pub stateful_sets: Vec<StatefulSet>,
50+
}
51+
3352
/// The [`RoleGroupConfig`] specialised for HDFS: the validated config is the
3453
/// per-role [`AnyNodeConfig`],
3554
pub type ValidatedRoleGroupConfig = RoleGroupConfig<

0 commit comments

Comments
 (0)