Skip to content

Commit 5b241e1

Browse files
authored
refactor: Build RBAC with recommended labels (#806)
* factor: add infallible rbac functions * regenerate nix * changelog * switch op-rs to main branch * rename resource names functions, add role parse test * replace role_name with From impls, add missing labels * added missed role/From instance * improve tests: placement and thoroughness * move object_meta to build step * derive the version label in tests from the operator version
1 parent 36fd270 commit 5b241e1

15 files changed

Lines changed: 529 additions & 226 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ All notable changes to this project will be documented in this file.
88

99
- Internal operator refactoring: introduce a build() step in the reconciler that
1010
assembles all relevant Kubernetes resources before anything is applied ([#801]).
11+
- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac`
12+
functions and carry the full set of recommended labels ([#806]).
13+
1114
- Bump stackable-operator to 0.114.0 ([#810]).
1215

1316
[#801]: https://github.com/stackabletech/hdfs-operator/pull/801
17+
[#806]: https://github.com/stackabletech/hdfs-operator/pull/806
1418
[#810]: https://github.com/stackabletech/hdfs-operator/pull/810
1519

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

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ tracing = "0.1"
3030
tracing-futures = { version = "0.2", features = ["futures-03"] }
3131

3232
[patch."https://github.com/stackabletech/operator-rs.git"]
33-
#stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" }
34-
#stackable-operator = { path = "../operator-rs/crates/stackable-operator" }
33+
# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" }
34+
# stackable-operator = { path = "../operator-rs/crates/stackable-operator" }

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ impl ContainerConfig {
237237

238238
// HDFS main container
239239
let main_container_config = Self::from(*role);
240-
let resource_names = cluster.resource_names(role, role_group_name);
240+
let resource_names = cluster.role_group_resource_names(role, role_group_name);
241241
let object_name = resource_names.qualified_role_group_name().to_string();
242242
let merged_config = &rolegroup_config.config;
243243

@@ -285,7 +285,7 @@ impl ContainerConfig {
285285
log_config,
286286
vector_aggregator_config_map_name,
287287
},
288-
&cluster.resource_names(role, role_group_name),
288+
&cluster.role_group_resource_names(role, role_group_name),
289289
&VECTOR_CONFIG_VOLUME_NAME,
290290
&VECTOR_LOG_VOLUME_NAME,
291291
EnvVarSet::new(),

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

Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashMap, str::FromStr};
1+
use std::collections::HashMap;
22

33
use snafu::{ResultExt, Snafu};
44
use stackable_operator::{
@@ -7,13 +7,15 @@ use stackable_operator::{
77
utils::cluster_info::KubernetesClusterInfo,
88
v2::{
99
builder::meta::ownerreference_from_resource,
10-
types::{common::Port, kubernetes::ServiceName, operator::RoleGroupName},
10+
types::{common::Port, operator::RoleGroupName},
1111
},
1212
};
1313

1414
use crate::{
15-
build_recommended_labels,
16-
controller::{KubernetesResources, ValidatedCluster},
15+
controller::{
16+
KubernetesResources, ValidatedCluster,
17+
build::resource::rbac::{build_role_binding, build_service_account},
18+
},
1719
crd::{
1820
HdfsNodeRole, HdfsPodRef,
1921
constants::{
@@ -32,7 +34,6 @@ use crate::{
3234
SERVICE_PORT_NAME_RPC,
3335
},
3436
},
35-
hdfs_controller::RESOURCE_MANAGER_HDFS_CONTROLLER,
3637
};
3738

3839
pub mod container;
@@ -74,17 +75,13 @@ pub enum Error {
7475
/// `cluster_info` carries static cluster information resolved at operator startup (e.g. the
7576
/// cluster domain used to build Kerberos principals), not a live client.
7677
///
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-
///
8078
/// The resources are returned as flat, unordered collections. The reconcile step re-groups the
8179
/// StatefulSets by role to preserve HDFS's ordered, rollout-gated deployment during upgrades. The
8280
/// discovery `ConfigMap` is deliberately not built here: it needs a live client to resolve
8381
/// listener addresses and is therefore handled in the reconcile step.
8482
pub fn build(
8583
cluster: &ValidatedCluster,
8684
cluster_info: &KubernetesClusterInfo,
87-
service_account_name: &str,
8885
) -> Result<KubernetesResources, Error> {
8986
let mut services = vec![];
9087
let mut config_maps = vec![];
@@ -126,7 +123,6 @@ pub fn build(
126123
role,
127124
role_group_name,
128125
rg_config,
129-
service_account_name,
130126
)
131127
.context(StatefulSetSnafu {
132128
role: *role,
@@ -145,6 +141,8 @@ pub fn build(
145141
config_maps,
146142
pod_disruption_budgets,
147143
stateful_sets,
144+
service_accounts: vec![build_service_account(cluster)],
145+
role_bindings: vec![build_role_binding(cluster)],
148146
})
149147
}
150148

@@ -168,15 +166,7 @@ pub(crate) fn pod_refs(cluster: &ValidatedCluster, role: &HdfsNodeRole) -> Vec<H
168166
.into_iter()
169167
.flatten()
170168
.flat_map(|(role_group_name, role_group)| {
171-
// The headless Service that governs the pods is named after the qualified role group
172-
// name (see `build::resource::service::rolegroup_headless_service`).
173-
let service_name = ServiceName::from_str(
174-
cluster
175-
.resource_names(role, role_group_name)
176-
.qualified_role_group_name()
177-
.as_ref(),
178-
)
179-
.expect("a qualified role group name is a valid Service name");
169+
let service_name = cluster.governing_service_name(role, role_group_name);
180170
let object_name = service_name.to_string();
181171
let namespace = cluster.namespace.clone();
182172
let ports = ports.clone();
@@ -191,6 +181,22 @@ pub(crate) fn pod_refs(cluster: &ValidatedCluster, role: &HdfsNodeRole) -> Vec<H
191181
.collect()
192182
}
193183

184+
/// Returns an [`ObjectMetaBuilder`] pre-filled with the namespace, the resource `name`, an owner
185+
/// reference back to the cluster, and the given recommended `labels`.
186+
pub(crate) fn object_meta(
187+
cluster: &ValidatedCluster,
188+
name: impl Into<String>,
189+
labels: Labels,
190+
) -> ObjectMetaBuilder {
191+
let mut builder = ObjectMetaBuilder::new();
192+
builder
193+
.name_and_namespace(cluster)
194+
.name(name)
195+
.ownerreference(ownerreference_from_resource(cluster, None, Some(true)))
196+
.with_labels(labels);
197+
builder
198+
}
199+
194200
/// Builds the common [`ObjectMetaBuilder`] shared by a role group's owned resources
195201
/// (the ConfigMap and the StatefulSet): name, namespace, owner reference and the
196202
/// recommended labels, all derived from the validated cluster.
@@ -203,28 +209,14 @@ pub(crate) fn rolegroup_metadata(
203209
role: &HdfsNodeRole,
204210
role_group_name: &RoleGroupName,
205211
) -> ObjectMetaBuilder {
206-
let role_name = role.to_string();
207-
let mut metadata = ObjectMetaBuilder::new();
208-
metadata
209-
.name_and_namespace(cluster)
210-
.name(
211-
cluster
212-
.resource_names(role, role_group_name)
213-
.qualified_role_group_name(),
214-
)
215-
.ownerreference(ownerreference_from_resource(cluster, None, Some(true)))
216-
.with_recommended_labels(&build_recommended_labels(
217-
cluster,
218-
RESOURCE_MANAGER_HDFS_CONTROLLER,
219-
&cluster.image.app_version_label_value,
220-
&role_name,
221-
role_group_name.as_ref(),
222-
))
223-
.expect(
224-
"the recommended labels are valid because the ValidatedCluster uses \
225-
fail-safe typed values",
226-
);
227-
metadata
212+
object_meta(
213+
cluster,
214+
cluster
215+
.role_group_resource_names(role, role_group_name)
216+
.qualified_role_group_name()
217+
.to_string(),
218+
cluster.recommended_labels(role, role_group_name),
219+
)
228220
}
229221

230222
/// The rolegroup selector labels (also used as `Service`/`StatefulSet` selectors) for
@@ -394,8 +386,7 @@ mod tests {
394386
#[test]
395387
fn build_produces_expected_resource_names() {
396388
let cluster = validated_cluster();
397-
let resources =
398-
build(&cluster, &cluster_info(), "hdfs-serviceaccount").expect("build succeeds");
389+
let resources = build(&cluster, &cluster_info()).expect("build succeeds");
399390

400391
assert_eq!(
401392
sorted_names(&resources.stateful_sets),
@@ -405,8 +396,19 @@ mod tests {
405396
"hdfs-namenode-default",
406397
]
407398
);
408-
// One headless and one metrics Service per role group.
409-
assert_eq!(resources.services.len(), 6);
399+
// One headless (un-suffixed, see `ValidatedCluster::governing_service_name`) and one
400+
// metrics Service per role group.
401+
assert_eq!(
402+
sorted_names(&resources.services),
403+
[
404+
"hdfs-datanode-default",
405+
"hdfs-datanode-default-metrics",
406+
"hdfs-journalnode-default",
407+
"hdfs-journalnode-default-metrics",
408+
"hdfs-namenode-default",
409+
"hdfs-namenode-default-metrics",
410+
]
411+
);
410412
assert_eq!(
411413
sorted_names(&resources.config_maps),
412414
[
@@ -420,5 +422,34 @@ mod tests {
420422
sorted_names(&resources.pod_disruption_budgets),
421423
["hdfs-datanode", "hdfs-journalnode", "hdfs-namenode"]
422424
);
425+
// The cluster-shared RBAC pair.
426+
assert_eq!(
427+
sorted_names(&resources.service_accounts),
428+
["hdfs-serviceaccount"]
429+
);
430+
assert_eq!(sorted_names(&resources.role_bindings), ["hdfs-rolebinding"]);
431+
}
432+
433+
/// Every StatefulSet's (immutable) `serviceName` must reference a headless Service that the
434+
/// build step actually produces — the pods' DNS names depend on the pair agreeing. Guards the
435+
/// coupling that `ValidatedCluster::governing_service_name` centralises.
436+
#[test]
437+
fn statefulset_service_name_references_built_service() {
438+
let cluster = validated_cluster();
439+
let resources = build(&cluster, &cluster_info()).expect("build succeeds");
440+
441+
let service_names = sorted_names(&resources.services);
442+
for stateful_set in &resources.stateful_sets {
443+
let service_name = stateful_set
444+
.spec
445+
.as_ref()
446+
.and_then(|spec| spec.service_name.as_deref())
447+
.expect("every StatefulSet sets serviceName");
448+
assert!(
449+
service_names.iter().any(|name| name == service_name),
450+
"StatefulSet references headless Service {service_name:?}, which is not built \
451+
(built Services: {service_names:?})"
452+
);
453+
}
423454
}
424455
}

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

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
11
//! Build the discovery `ConfigMap` for the HdfsCluster.
22
3+
use std::str::FromStr;
4+
35
use snafu::{ResultExt, Snafu};
46
use stackable_operator::{
5-
builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder},
7+
builder::configmap::ConfigMapBuilder,
68
k8s_openapi::api::core::v1::ConfigMap,
79
utils::cluster_info::KubernetesClusterInfo,
8-
v2::builder::meta::ownerreference_from_resource,
10+
v2::{
11+
kvp::label::recommended_labels,
12+
types::operator::{ControllerName, RoleGroupName},
13+
},
914
};
1015

1116
use crate::{
12-
build_recommended_labels,
1317
controller::{
1418
ValidatedCluster,
1519
build::{
1620
kerberos::KerberosConfig,
21+
object_meta,
1722
properties::{
1823
ConfigFileName, core_site::CoreSiteConfigBuilder, hdfs_site::HdfsSiteConfigBuilder,
1924
},
2025
},
26+
operator_name, product_name,
2127
},
2228
crd::{HdfsNodeRole, HdfsPodRef},
2329
hdfs_controller::HDFS_CONTROLLER_NAME,
2430
};
2531

2632
type Result<T, E = Error> = std::result::Result<T, E>;
2733

34+
stackable_operator::constant!(DISCOVERY_ROLE_GROUP: RoleGroupName = "discovery");
35+
2836
#[derive(Snafu, Debug)]
2937
#[allow(clippy::enum_variant_names)]
3038
pub enum Error {
3139
#[snafu(display("failed to build ConfigMap"))]
3240
BuildConfigMap {
3341
source: stackable_operator::builder::configmap::Error,
3442
},
35-
36-
#[snafu(display("failed to build object meta data"))]
37-
ObjectMeta {
38-
source: stackable_operator::builder::meta::Error,
39-
},
4043
}
4144

4245
/// Creates a discovery config map containing the `hdfs-site.xml` and `core-site.xml`
@@ -50,18 +53,22 @@ pub fn build_discovery_config_map(
5053
cluster_info: &KubernetesClusterInfo,
5154
namenode_podrefs: &[HdfsPodRef],
5255
) -> Result<ConfigMap> {
53-
let metadata = ObjectMetaBuilder::new()
54-
.name_and_namespace(cluster)
55-
.ownerreference(ownerreference_from_resource(cluster, None, Some(true)))
56-
.with_recommended_labels(&build_recommended_labels(
57-
cluster,
58-
HDFS_CONTROLLER_NAME,
59-
&cluster.image.app_version_label_value,
60-
&HdfsNodeRole::Name.to_string(),
61-
"discovery",
62-
))
63-
.context(ObjectMetaSnafu)?
64-
.build();
56+
// The discovery ConfigMap deliberately deviates from the standard resource identity: it is
57+
// labelled with the `hdfs-controller` controller name (NOT `controller_name()`, i.e.
58+
// `hdfs-operator-hdfs-controller` like the role-group resources), which keeps it outside their
59+
// `ClusterResources` orphan-matching, and with the namenode role plus a `discovery` role-group.
60+
let labels = recommended_labels(
61+
cluster,
62+
&product_name(),
63+
&cluster.product_version,
64+
&operator_name(),
65+
&ControllerName::from_str(HDFS_CONTROLLER_NAME)
66+
.expect("the hdfs controller name is a valid label value"),
67+
&HdfsNodeRole::Name.into(),
68+
&DISCOVERY_ROLE_GROUP,
69+
);
70+
71+
let metadata = object_meta(cluster, cluster.name.clone(), labels).build();
6572

6673
ConfigMapBuilder::new()
6774
.metadata(metadata)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod config_map;
22
pub mod discovery;
33
pub mod pdb;
4+
pub mod rbac;
45
pub mod service;
56
pub mod statefulset;

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
use std::{
2-
cmp::{max, min},
3-
str::FromStr,
4-
};
1+
use std::cmp::{max, min};
52

63
use stackable_operator::{
74
k8s_openapi::api::policy::v1::PodDisruptionBudget,
@@ -28,8 +25,7 @@ pub fn build_pdb(cluster: &ValidatedCluster, role: &HdfsNodeRole) -> Option<PodD
2825
),
2926
HdfsNodeRole::Journal => max_unavailable_journal_nodes(),
3027
});
31-
let role_name =
32-
RoleName::from_str(&role.to_string()).expect("a HdfsNodeRole is a valid role name");
28+
let role_name: RoleName = role.into();
3329
let pdb = pod_disruption_budget_builder_with_role(
3430
cluster,
3531
&product_name(),

0 commit comments

Comments
 (0)