Skip to content

Commit 3327a68

Browse files
committed
factor: add infallible rbac functions
1 parent 8af09ec commit 3327a68

11 files changed

Lines changed: 327 additions & 229 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ tracing-futures = { version = "0.2", features = ["futures-03"] }
3131

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

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

Lines changed: 111 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1-
use std::{collections::HashMap, str::FromStr};
1+
use std::collections::HashMap;
22

33
use snafu::{ResultExt, Snafu};
44
use stackable_operator::{
55
builder::meta::ObjectMetaBuilder,
66
kvp::{LabelError, Labels},
77
utils::cluster_info::KubernetesClusterInfo,
8-
v2::{
9-
builder::meta::ownerreference_from_resource,
10-
types::{common::Port, kubernetes::ServiceName, operator::RoleGroupName},
11-
},
8+
v2::types::{common::Port, operator::RoleGroupName},
129
};
1310

1411
use crate::{
15-
build_recommended_labels,
16-
controller::{KubernetesResources, ValidatedCluster},
12+
controller::{
13+
KubernetesResources, ValidatedCluster,
14+
build::resource::rbac::{build_role_binding, build_service_account},
15+
},
1716
crd::{
1817
HdfsNodeRole, HdfsPodRef,
1918
constants::{
@@ -32,7 +31,6 @@ use crate::{
3231
SERVICE_PORT_NAME_RPC,
3332
},
3433
},
35-
hdfs_controller::RESOURCE_MANAGER_HDFS_CONTROLLER,
3634
};
3735

3836
pub mod container;
@@ -74,17 +72,13 @@ pub enum Error {
7472
/// `cluster_info` carries static cluster information resolved at operator startup (e.g. the
7573
/// cluster domain used to build Kerberos principals), not a live client.
7674
///
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-
///
8075
/// The resources are returned as flat, unordered collections. The reconcile step re-groups the
8176
/// StatefulSets by role to preserve HDFS's ordered, rollout-gated deployment during upgrades. The
8277
/// discovery `ConfigMap` is deliberately not built here: it needs a live client to resolve
8378
/// listener addresses and is therefore handled in the reconcile step.
8479
pub fn build(
8580
cluster: &ValidatedCluster,
8681
cluster_info: &KubernetesClusterInfo,
87-
service_account_name: &str,
8882
) -> Result<KubernetesResources, Error> {
8983
let mut services = vec![];
9084
let mut config_maps = vec![];
@@ -126,7 +120,6 @@ pub fn build(
126120
role,
127121
role_group_name,
128122
rg_config,
129-
service_account_name,
130123
)
131124
.context(StatefulSetSnafu {
132125
role: *role,
@@ -145,6 +138,8 @@ pub fn build(
145138
config_maps,
146139
pod_disruption_budgets,
147140
stateful_sets,
141+
service_accounts: vec![build_service_account(cluster)],
142+
role_bindings: vec![build_role_binding(cluster)],
148143
})
149144
}
150145

@@ -168,15 +163,7 @@ pub(crate) fn pod_refs(cluster: &ValidatedCluster, role: &HdfsNodeRole) -> Vec<H
168163
.into_iter()
169164
.flatten()
170165
.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");
166+
let service_name = cluster.governing_service_name(role, role_group_name);
180167
let object_name = service_name.to_string();
181168
let namespace = cluster.namespace.clone();
182169
let ports = ports.clone();
@@ -203,28 +190,13 @@ pub(crate) fn rolegroup_metadata(
203190
role: &HdfsNodeRole,
204191
role_group_name: &RoleGroupName,
205192
) -> 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
193+
cluster.object_meta(
194+
cluster
195+
.resource_names(role, role_group_name)
196+
.qualified_role_group_name()
197+
.to_string(),
198+
cluster.recommended_labels(role, role_group_name),
199+
)
228200
}
229201

230202
/// The rolegroup selector labels (also used as `Service`/`StatefulSet` selectors) for
@@ -373,10 +345,15 @@ fn role_data_ports(role: &HdfsNodeRole, https_enabled: bool) -> Vec<(String, Por
373345

374346
#[cfg(test)]
375347
mod tests {
348+
use std::collections::BTreeMap;
349+
376350
use stackable_operator::kube::Resource;
377351

378352
use super::build;
379-
use crate::controller::build::properties::test_support::{cluster_info, validated_cluster};
353+
use crate::{
354+
controller::build::properties::test_support::{self, cluster_info, validated_cluster},
355+
test_support::deserialize_and_validate_cluster,
356+
};
380357

381358
/// The sorted `metadata.name`s of a resource collection.
382359
fn sorted_names(resources: &[impl Resource]) -> Vec<String> {
@@ -394,8 +371,7 @@ mod tests {
394371
#[test]
395372
fn build_produces_expected_resource_names() {
396373
let cluster = validated_cluster();
397-
let resources =
398-
build(&cluster, &cluster_info(), "hdfs-serviceaccount").expect("build succeeds");
374+
let resources = build(&cluster, &cluster_info()).expect("build succeeds");
399375

400376
assert_eq!(
401377
sorted_names(&resources.stateful_sets),
@@ -405,8 +381,19 @@ mod tests {
405381
"hdfs-namenode-default",
406382
]
407383
);
408-
// One headless and one metrics Service per role group.
409-
assert_eq!(resources.services.len(), 6);
384+
// One headless (un-suffixed, see `ValidatedCluster::governing_service_name`) and one
385+
// metrics Service per role group.
386+
assert_eq!(
387+
sorted_names(&resources.services),
388+
[
389+
"hdfs-datanode-default",
390+
"hdfs-datanode-default-metrics",
391+
"hdfs-journalnode-default",
392+
"hdfs-journalnode-default-metrics",
393+
"hdfs-namenode-default",
394+
"hdfs-namenode-default-metrics",
395+
]
396+
);
410397
assert_eq!(
411398
sorted_names(&resources.config_maps),
412399
[
@@ -421,4 +408,79 @@ mod tests {
421408
["hdfs-datanode", "hdfs-journalnode", "hdfs-namenode"]
422409
);
423410
}
411+
412+
/// Every StatefulSet's (immutable) `serviceName` must reference a headless Service that the
413+
/// build step actually produces — the pods' DNS names depend on the pair agreeing. Guards the
414+
/// coupling that `ValidatedCluster::governing_service_name` centralises.
415+
#[test]
416+
fn statefulset_service_name_references_built_service() {
417+
let cluster = validated_cluster();
418+
let resources = build(&cluster, &cluster_info()).expect("build succeeds");
419+
420+
let service_names = sorted_names(&resources.services);
421+
for stateful_set in &resources.stateful_sets {
422+
let service_name = stateful_set
423+
.spec
424+
.as_ref()
425+
.and_then(|spec| spec.service_name.as_deref())
426+
.expect("every StatefulSet sets serviceName");
427+
assert!(
428+
service_names.iter().any(|name| name == service_name),
429+
"StatefulSet references headless Service {service_name:?}, which is not built \
430+
(built Services: {service_names:?})"
431+
);
432+
}
433+
}
434+
435+
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
436+
/// accidental drift. The cluster name deliberately differs from the product name so that
437+
/// swapped `name`/`instance` label values cannot pass unnoticed (the shared fixture is named
438+
/// `hdfs`, which would mask exactly that swap).
439+
#[test]
440+
fn build_produces_rbac() {
441+
let cluster = deserialize_and_validate_cluster(
442+
&test_support::MINIMAL_HDFS_YAML.replace("name: hdfs", "name: my-hdfs"),
443+
);
444+
let resources = build(&cluster, &cluster_info()).expect("build succeeds");
445+
446+
assert_eq!(
447+
sorted_names(&resources.service_accounts),
448+
["my-hdfs-serviceaccount"]
449+
);
450+
assert_eq!(
451+
sorted_names(&resources.role_bindings),
452+
["my-hdfs-rolebinding"]
453+
);
454+
455+
let expected_labels = BTreeMap::from(
456+
[
457+
("app.kubernetes.io/component", "none"),
458+
("app.kubernetes.io/instance", "my-hdfs"),
459+
(
460+
"app.kubernetes.io/managed-by",
461+
"hdfs.stackable.tech_hdfs-operator-hdfs-controller",
462+
),
463+
("app.kubernetes.io/name", "hdfs"),
464+
("app.kubernetes.io/role-group", "none"),
465+
("app.kubernetes.io/version", "3.4.0-stackable0.0.0-dev"),
466+
("stackable.tech/vendor", "Stackable"),
467+
]
468+
.map(|(key, value)| (key.to_string(), value.to_string())),
469+
);
470+
let service_account = resources
471+
.service_accounts
472+
.first()
473+
.expect("a ServiceAccount is built");
474+
assert_eq!(
475+
service_account.metadata.labels,
476+
Some(expected_labels.clone())
477+
);
478+
479+
let role_binding = resources
480+
.role_bindings
481+
.first()
482+
.expect("a RoleBinding is built");
483+
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
484+
assert_eq!(role_binding.role_ref.name, "hdfs-clusterrole");
485+
}
424486
}

0 commit comments

Comments
 (0)