Skip to content

Commit 751c943

Browse files
committed
refactor rbac (service account and role binding), added tests
1 parent 43a7908 commit 751c943

9 files changed

Lines changed: 140 additions & 15554 deletions

File tree

Cargo.nix

Lines changed: 0 additions & 15484 deletions
This file was deleted.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ tokio = { version = "1.52", features = ["full"] }
2929
tracing = "0.1"
3030

3131
[patch."https://github.com/stackabletech/operator-rs.git"]
32-
# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" }
32+
stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "feat/smooth-operator/build-rbac" }
3333
# stackable-operator = { path = "../operator-rs/crates/stackable-operator" }

crate-hashes.json

Lines changed: 0 additions & 11 deletions
This file was deleted.

rust/operator-binary/src/controller.rs

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ use stackable_operator::{
1515
affinity::StackableAffinity,
1616
product_image_selection::ResolvedProductImage,
1717
random_secret_creation::{self, create_random_secret_if_not_exists},
18-
rbac::build_rbac_resources,
1918
resources::{NoRuntimeLimits, Resources},
2019
},
2120
crd::listener,
2221
k8s_openapi::api::{
2322
apps::v1::{Deployment, StatefulSet},
24-
core::v1::{ConfigMap, Secret, Service},
23+
core::v1::{ConfigMap, Secret, Service, ServiceAccount},
2524
policy::v1::PodDisruptionBudget,
25+
rbac::v1::RoleBinding,
2626
},
2727
kube::{
28-
Resource, ResourceExt,
28+
Resource,
2929
api::ObjectMeta,
3030
core::{DeserializeGuard, error_boundary},
3131
runtime::controller::Action,
@@ -44,7 +44,7 @@ use stackable_operator::{
4444
kvp::label::{recommended_labels, role_group_selector},
4545
product_logging::framework::{ValidatedContainerLogConfigChoice, VectorContainerLogConfig},
4646
role_group_utils::ResourceNames,
47-
role_utils::{GenericCommonConfig, RoleGroupConfig},
47+
role_utils::{self, GenericCommonConfig, RoleGroupConfig},
4848
types::{
4949
kubernetes::{ListenerClassName, ListenerName, NamespaceName, Uid},
5050
operator::{
@@ -94,6 +94,8 @@ pub struct KubernetesResources {
9494
pub listeners: Vec<listener::v1alpha1::Listener>,
9595
pub config_maps: Vec<ConfigMap>,
9696
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
97+
pub service_accounts: Vec<ServiceAccount>,
98+
pub role_bindings: Vec<RoleBinding>,
9799
}
98100

99101
/// Per-role configuration extracted during validation.
@@ -238,6 +240,15 @@ impl ValidatedCluster {
238240
}
239241
}
240242

243+
/// Type-safe names for the per-cluster RBAC resources: the ServiceAccount shared by all
244+
/// Pods, its (namespaced) RoleBinding, and the operator-deployed ClusterRole it binds.
245+
pub fn rbac_resource_names(&self) -> role_utils::ResourceNames {
246+
role_utils::ResourceNames {
247+
cluster_name: self.name.clone(),
248+
product_name: product_name(),
249+
}
250+
}
251+
241252
pub fn recommended_labels(
242253
&self,
243254
role: &SupersetRole,
@@ -409,27 +420,6 @@ pub enum Error {
409420
source: stackable_operator::client::Error,
410421
},
411422

412-
#[snafu(display("failed to patch service account"))]
413-
ApplyServiceAccount {
414-
source: stackable_operator::cluster_resources::Error,
415-
},
416-
417-
#[snafu(display("failed to patch role binding"))]
418-
ApplyRoleBinding {
419-
source: stackable_operator::cluster_resources::Error,
420-
},
421-
422-
#[snafu(display("failed to build RBAC objects"))]
423-
BuildRBACObjects {
424-
source: stackable_operator::commons::rbac::Error,
425-
},
426-
427-
#[snafu(display("failed to get required Labels"))]
428-
GetRequiredLabels {
429-
source:
430-
stackable_operator::kvp::KeyValuePairError<stackable_operator::kvp::LabelValueError>,
431-
},
432-
433423
#[snafu(display("SupersetCluster object is invalid"))]
434424
InvalidSupersetCluster {
435425
source: error_boundary::InvalidObject,
@@ -504,24 +494,6 @@ pub async fn reconcile_superset(
504494
&superset.spec.object_overrides,
505495
);
506496

507-
let (rbac_sa, rbac_rolebinding) = build_rbac_resources(
508-
superset,
509-
APP_NAME,
510-
cluster_resources
511-
.get_required_labels()
512-
.context(GetRequiredLabelsSnafu)?,
513-
)
514-
.context(BuildRBACObjectsSnafu)?;
515-
516-
let rbac_sa = cluster_resources
517-
.add(client, rbac_sa)
518-
.await
519-
.context(ApplyServiceAccountSnafu)?;
520-
cluster_resources
521-
.add(client, rbac_rolebinding)
522-
.await
523-
.context(ApplyRoleBindingSnafu)?;
524-
525497
// TODO: Can be removed after SDP 26.7 is released (it's only a migration from 26.3 - 26.7)
526498
// (don't forget about the snafu Error variants).
527499
// Removal is tracked in https://github.com/stackabletech/superset-operator/issues/755
@@ -536,14 +508,26 @@ pub async fn reconcile_superset(
536508
.await
537509
.context(CreateSecretKeySecretSnafu)?;
538510

539-
let resources = build::build(&validated, &rbac_sa.name_any()).context(BuildResourcesSnafu)?;
511+
let resources = build::build(&validated).context(BuildResourcesSnafu)?;
540512

541513
let mut statefulset_cond_builder = StatefulSetConditionBuilder::default();
542514
let mut deployment_cond_builder = DeploymentConditionBuilder::default();
543515

544516
// The StatefulSets/Deployments are applied last, so every ConfigMap and Secret they mount
545517
// already exists — otherwise a changed mount would restart the Pods.
546518
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
519+
for service_account in resources.service_accounts {
520+
cluster_resources
521+
.add(client, service_account)
522+
.await
523+
.context(ApplyResourceSnafu)?;
524+
}
525+
for role_binding in resources.role_bindings {
526+
cluster_resources
527+
.add(client, role_binding)
528+
.await
529+
.context(ApplyResourceSnafu)?;
530+
}
547531
for service in resources.services {
548532
cluster_resources
549533
.add(client, service)

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

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::{
1313
deployment::build_rolegroup_deployment,
1414
listener::build_group_listener,
1515
pdb::build_pdb,
16+
rbac::{build_role_binding, build_service_account},
1617
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
1718
statefulset::build_node_rolegroup_statefulset,
1819
},
@@ -54,13 +55,7 @@ pub enum Error {
5455
}
5556

5657
/// Builds every Kubernetes resource for the given validated cluster.
57-
///
58-
/// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under.
59-
/// The RBAC resources themselves are built and applied separately in the reconcile step.
60-
pub fn build(
61-
cluster: &ValidatedCluster,
62-
service_account_name: &str,
63-
) -> Result<KubernetesResources, Error> {
58+
pub fn build(cluster: &ValidatedCluster) -> Result<KubernetesResources, Error> {
6459
let mut stateful_sets = vec![];
6560
let mut deployments = vec![];
6661
let mut services = vec![];
@@ -110,7 +105,6 @@ pub fn build(
110105
superset_role,
111106
role_group_name,
112107
rolegroup_config,
113-
service_account_name,
114108
)
115109
.context(StatefulSetSnafu {
116110
role_group: role_group_name.clone(),
@@ -124,7 +118,6 @@ pub fn build(
124118
superset_role,
125119
role_group_name,
126120
rolegroup_config,
127-
service_account_name,
128121
)
129122
.context(DeploymentSnafu {
130123
role_group: role_group_name.clone(),
@@ -162,11 +155,15 @@ pub fn build(
162155
listeners,
163156
config_maps,
164157
pod_disruption_budgets,
158+
service_accounts: vec![build_service_account(cluster)],
159+
role_bindings: vec![build_role_binding(cluster)],
165160
})
166161
}
167162

168163
#[cfg(test)]
169164
mod tests {
165+
use std::collections::BTreeMap;
166+
170167
use stackable_operator::{kube::Resource, utils::yaml_from_str_singleton_map};
171168

172169
use super::build;
@@ -230,7 +227,7 @@ mod tests {
230227
#[test]
231228
fn build_produces_expected_resource_names() {
232229
let cluster = validated_cluster();
233-
let resources = build(&cluster, "simple-superset-serviceaccount").expect("build succeeds");
230+
let resources = build(&cluster).expect("build succeeds");
234231

235232
assert_eq!(
236233
sorted_names(&resources.stateful_sets),
@@ -263,4 +260,53 @@ mod tests {
263260
]
264261
);
265262
}
263+
264+
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
265+
/// accidental drift. The fixture's cluster name deliberately differs from the product name so
266+
/// that swapped `name`/`instance` label values cannot pass unnoticed.
267+
#[test]
268+
fn build_produces_rbac() {
269+
let cluster = validated_cluster();
270+
let resources = build(&cluster).expect("build succeeds");
271+
272+
assert_eq!(
273+
sorted_names(&resources.service_accounts),
274+
["simple-superset-serviceaccount"]
275+
);
276+
assert_eq!(
277+
sorted_names(&resources.role_bindings),
278+
["simple-superset-rolebinding"]
279+
);
280+
281+
let expected_labels = BTreeMap::from(
282+
[
283+
("app.kubernetes.io/component", "none"),
284+
("app.kubernetes.io/instance", "simple-superset"),
285+
(
286+
"app.kubernetes.io/managed-by",
287+
"superset.stackable.tech_supersetcluster",
288+
),
289+
("app.kubernetes.io/name", "superset"),
290+
("app.kubernetes.io/role-group", "none"),
291+
("app.kubernetes.io/version", "4.1.4-stackable0.0.0-dev"),
292+
("stackable.tech/vendor", "Stackable"),
293+
]
294+
.map(|(key, value)| (key.to_string(), value.to_string())),
295+
);
296+
let service_account = resources
297+
.service_accounts
298+
.first()
299+
.expect("a ServiceAccount is built");
300+
assert_eq!(
301+
service_account.metadata.labels,
302+
Some(expected_labels.clone())
303+
);
304+
305+
let role_binding = resources
306+
.role_bindings
307+
.first()
308+
.expect("a RoleBinding is built");
309+
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
310+
assert_eq!(role_binding.role_ref.name, "superset-clusterrole");
311+
}
266312
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ pub fn build_rolegroup_deployment(
6464
superset_role: &SupersetRole,
6565
role_group_name: &RoleGroupName,
6666
rolegroup_config: &SupersetRoleGroupConfig,
67-
sa_name: &str,
6867
) -> Result<Deployment> {
6968
let merged_config = &rolegroup_config.config;
7069

@@ -102,7 +101,12 @@ pub fn build_rolegroup_deployment(
102101
.build(),
103102
)
104103
.affinity(&merged_config.affinity)
105-
.service_account_name(sa_name);
104+
.service_account_name(
105+
validated
106+
.rbac_resource_names()
107+
.service_account_name()
108+
.to_string(),
109+
);
106110

107111
let mut superset_cb = super::build_superset_container_builder(validated, rolegroup_config)
108112
.context(BuildContainerSnafu)?;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub mod config_map;
4949
pub mod deployment;
5050
pub mod listener;
5151
pub mod pdb;
52+
pub mod rbac;
5253
pub mod service;
5354
pub mod statefulset;
5455

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.rbac_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.rbac_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/statefulset.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ pub fn build_node_rolegroup_statefulset(
9494
superset_role: &SupersetRole,
9595
role_group_name: &RoleGroupName,
9696
rolegroup_config: &SupersetRoleGroupConfig,
97-
sa_name: &str,
9897
) -> Result<StatefulSet> {
9998
let merged_config = &rolegroup_config.config;
10099

@@ -118,7 +117,12 @@ pub fn build_node_rolegroup_statefulset(
118117
.build(),
119118
)
120119
.affinity(&merged_config.affinity)
121-
.service_account_name(sa_name);
120+
.service_account_name(
121+
validated
122+
.rbac_resource_names()
123+
.service_account_name()
124+
.to_string(),
125+
);
122126

123127
let mut superset_cb = super::build_superset_container_builder(validated, rolegroup_config)
124128
.context(BuildContainerSnafu)?;

0 commit comments

Comments
 (0)