Skip to content

Commit be2ebff

Browse files
authored
refactor: Build RBAC with recommended labels (#782)
* factor: add infallible rbac functions * switch to op-rs branch * changelog * rename resource names functions, add role parse test * reference op-rs 0.114.0 * add impls for role/from and parsing test. Fixed comment
1 parent e6c5ab5 commit be2ebff

11 files changed

Lines changed: 205 additions & 93 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
- Internal operator refactoring: introduce a build() step in the reconciler that
88
assembles all relevant Kubernetes resources before anything is applied ([#776]).
9+
- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac`
10+
functions and carry the full set of recommended labels ([#782]).
911
- Bump stackable-operator to 0.114.0 ([#786]).
1012

1113
[#776]: https://github.com/stackabletech/hbase-operator/pull/776
14+
[#782]: https://github.com/stackabletech/hbase-operator/pull/782
1215
[#786]: https://github.com/stackabletech/hbase-operator/pull/786
1316

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

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

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Builders that turn a [`ValidatedCluster`](crate::controller::ValidatedCluster) into
1+
//! Builders that turn a [`ValidatedCluster`] into
22
//! Kubernetes resources.
33
44
use std::str::FromStr;
@@ -15,6 +15,7 @@ use crate::{
1515
config_map::{self, build_rolegroup_config_map},
1616
discovery::{self, build_discovery_config_map},
1717
pdb::build_pdb,
18+
rbac::{build_role_binding, build_service_account},
1819
service::{build_rolegroup_metrics_service, build_rolegroup_service},
1920
statefulset::{self, build_rolegroup_statefulset},
2021
},
@@ -50,13 +51,10 @@ pub enum Error {
5051
///
5152
/// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
5253
/// 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).
54+
/// failures only. `cluster_info` is static cluster metadata (not a client call).
5655
pub fn build(
5756
cluster: &ValidatedCluster,
5857
cluster_info: &KubernetesClusterInfo,
59-
service_account_name: &str,
6058
) -> Result<KubernetesResources, Error> {
6159
let mut stateful_sets = vec![];
6260
let mut services = vec![];
@@ -83,17 +81,11 @@ pub fn build(
8381
})?,
8482
);
8583
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-
})?,
84+
build_rolegroup_statefulset(cluster, hbase_role, role_group_name, rg_config)
85+
.with_context(|_| StatefulSetSnafu {
86+
hbase_role: hbase_role.clone(),
87+
role_group: role_group_name.clone(),
88+
})?,
9789
);
9890
}
9991

@@ -113,6 +105,8 @@ pub fn build(
113105
services,
114106
config_maps,
115107
pod_disruption_budgets,
108+
service_accounts: vec![build_service_account(cluster)],
109+
role_bindings: vec![build_role_binding(cluster)],
116110
})
117111
}
118112

@@ -127,6 +121,8 @@ pub mod role;
127121

128122
#[cfg(test)]
129123
mod tests {
124+
use std::collections::BTreeMap;
125+
130126
use stackable_operator::kube::Resource;
131127

132128
use super::build;
@@ -146,8 +142,7 @@ mod tests {
146142
fn build_produces_expected_resource_names() {
147143
let cluster = test_utils::validated_cluster();
148144
let cluster_info = test_utils::cluster_info();
149-
let resources =
150-
build(&cluster, &cluster_info, "hbase-serviceaccount").expect("build succeeds");
145+
let resources = build(&cluster, &cluster_info).expect("build succeeds");
151146

152147
// One StatefulSet per role group (one `default` group for each of the three roles).
153148
assert_eq!(
@@ -186,4 +181,58 @@ mod tests {
186181
["hbase-master", "hbase-regionserver", "hbase-restserver"]
187182
);
188183
}
184+
185+
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
186+
/// accidental drift. The cluster name deliberately differs from the product name so that
187+
/// swapped `name`/`instance` label values cannot pass unnoticed (the shared fixture is named
188+
/// `hbase`, which would mask exactly that swap).
189+
#[test]
190+
fn build_produces_rbac() {
191+
let hbase = test_utils::hbase_from_yaml(
192+
&test_utils::MINIMAL_HBASE_YAML.replace("name: hbase", "name: my-hbase"),
193+
);
194+
let cluster = test_utils::validated_cluster_from(&hbase);
195+
let cluster_info = test_utils::cluster_info();
196+
let resources = build(&cluster, &cluster_info).expect("build succeeds");
197+
198+
assert_eq!(
199+
sorted_names(&resources.service_accounts),
200+
["my-hbase-serviceaccount"]
201+
);
202+
assert_eq!(
203+
sorted_names(&resources.role_bindings),
204+
["my-hbase-rolebinding"]
205+
);
206+
207+
let expected_labels = BTreeMap::from(
208+
[
209+
("app.kubernetes.io/component", "none"),
210+
("app.kubernetes.io/instance", "my-hbase"),
211+
(
212+
"app.kubernetes.io/managed-by",
213+
"hbase.stackable.com_hbasecluster",
214+
),
215+
("app.kubernetes.io/name", "hbase"),
216+
("app.kubernetes.io/role-group", "none"),
217+
("app.kubernetes.io/version", "2.6.3-stackable0.0.0-dev"),
218+
("stackable.tech/vendor", "Stackable"),
219+
]
220+
.map(|(key, value)| (key.to_string(), value.to_string())),
221+
);
222+
let service_account = resources
223+
.service_accounts
224+
.first()
225+
.expect("a ServiceAccount is built");
226+
assert_eq!(
227+
service_account.metadata.labels,
228+
Some(expected_labels.clone())
229+
);
230+
231+
let role_binding = resources
232+
.role_bindings
233+
.first()
234+
.expect("a RoleBinding is built");
235+
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
236+
assert_eq!(role_binding.role_ref.name, "hbase-clusterrole");
237+
}
189238
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ pub fn build_rolegroup_config_map(
111111
let cm_metadata = cluster
112112
.object_meta(
113113
cluster
114-
.resource_names(role, role_group_name)
114+
.role_group_resource_names(role, role_group_name)
115115
.role_group_config_map()
116116
.to_string(),
117117
role,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
//! Builders that turn a [`ValidatedCluster`](crate::controller::ValidatedCluster) into
2-
//! Kubernetes resources.
1+
//! Builders for individual Kubernetes resources (one module per resource type).
32
43
pub mod config_map;
54
pub mod discovery;
65
pub mod listener;
76
pub mod pdb;
7+
pub mod rbac;
88
pub mod service;
99
pub mod statefulset;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub fn build_pdb(
2727
let pdb = pod_disruption_budget_builder_with_role(
2828
cluster,
2929
&product_name(),
30-
&ValidatedCluster::role_name(role),
30+
&role.into(),
3131
&operator_name(),
3232
&controller_name(),
3333
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! Builds the RBAC resources (ServiceAccount + RoleBinding) shared by all role groups.
2+
3+
use std::str::FromStr;
4+
5+
use stackable_operator::{
6+
k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding},
7+
kvp::Labels,
8+
v2::{
9+
rbac,
10+
types::operator::{RoleGroupName, RoleName},
11+
},
12+
};
13+
14+
use crate::controller::ValidatedCluster;
15+
16+
stackable_operator::constant!(NONE_ROLE_NAME: RoleName = "none");
17+
stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none");
18+
19+
/// Builds the [`ServiceAccount`] that the role-group Pods run under.
20+
pub fn build_service_account(cluster: &ValidatedCluster) -> ServiceAccount {
21+
rbac::build_service_account(
22+
cluster,
23+
&cluster.cluster_resource_names(),
24+
rbac_labels(cluster),
25+
)
26+
}
27+
28+
/// Builds the [`RoleBinding`] that binds the [`ServiceAccount`] from [`build_service_account`] to
29+
/// the operator-deployed ClusterRole.
30+
pub fn build_role_binding(cluster: &ValidatedCluster) -> RoleBinding {
31+
rbac::build_role_binding(
32+
cluster,
33+
&cluster.cluster_resource_names(),
34+
rbac_labels(cluster),
35+
)
36+
}
37+
38+
/// Both resources are shared by the whole cluster rather than tied to a role or role group, so
39+
/// the recommended labels carry `none` for both values.
40+
fn rbac_labels(cluster: &ValidatedCluster) -> Labels {
41+
cluster.recommended_labels_for(&NONE_ROLE_NAME, &NONE_ROLE_GROUP_NAME)
42+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub fn build_rolegroup_service(
3535
metadata: cluster
3636
.object_meta(
3737
cluster
38-
.resource_names(hbase_role, role_group_name)
38+
.role_group_resource_names(hbase_role, role_group_name)
3939
.headless_service_name()
4040
.to_string(),
4141
hbase_role,
@@ -77,7 +77,7 @@ pub fn build_rolegroup_metrics_service(
7777
metadata: cluster
7878
.object_meta(
7979
cluster
80-
.resource_names(hbase_role, role_group_name)
80+
.role_group_resource_names(hbase_role, role_group_name)
8181
.metrics_service_name()
8282
.to_string(),
8383
hbase_role,

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,11 @@ pub fn build_rolegroup_statefulset(
106106
hbase_role: &HbaseRole,
107107
role_group_name: &RoleGroupName,
108108
validated_rg_config: &HbaseRoleGroupConfig,
109-
service_account_name: &str,
110109
) -> Result<StatefulSet> {
111110
let resolved_product_image = &cluster.image;
112111
let merged_config = &validated_rg_config.config.config;
113112
let logging = &validated_rg_config.config.logging;
114-
let resource_names = cluster.resource_names(hbase_role, role_group_name);
113+
let resource_names = cluster.role_group_resource_names(hbase_role, role_group_name);
115114
let https_enabled = cluster.has_https_enabled();
116115

117116
let ports = hbase_role
@@ -239,7 +238,12 @@ pub fn build_rolegroup_statefulset(
239238
)),
240239
)
241240
.context(AddVolumeSnafu)?
242-
.service_account_name(service_account_name)
241+
.service_account_name(
242+
cluster
243+
.cluster_resource_names()
244+
.service_account_name()
245+
.to_string(),
246+
)
243247
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
244248

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

0 commit comments

Comments
 (0)