Skip to content

Commit a0f8450

Browse files
committed
factor: add infallible rbac functions
1 parent 29d45fb commit a0f8450

8 files changed

Lines changed: 166 additions & 84 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 = "0.1"
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: 66 additions & 15 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
},
@@ -56,7 +57,6 @@ pub enum Error {
5657
pub fn build(
5758
cluster: &ValidatedCluster,
5859
cluster_info: &KubernetesClusterInfo,
59-
service_account_name: &str,
6060
) -> Result<KubernetesResources, Error> {
6161
let mut stateful_sets = vec![];
6262
let mut services = vec![];
@@ -83,17 +83,11 @@ pub fn build(
8383
})?,
8484
);
8585
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-
})?,
86+
build_rolegroup_statefulset(cluster, hbase_role, role_group_name, rg_config)
87+
.with_context(|_| StatefulSetSnafu {
88+
hbase_role: hbase_role.clone(),
89+
role_group: role_group_name.clone(),
90+
})?,
9791
);
9892
}
9993

@@ -113,6 +107,8 @@ pub fn build(
113107
services,
114108
config_maps,
115109
pod_disruption_budgets,
110+
service_accounts: vec![build_service_account(cluster)],
111+
role_bindings: vec![build_role_binding(cluster)],
116112
})
117113
}
118114

@@ -127,6 +123,8 @@ pub mod role;
127123

128124
#[cfg(test)]
129125
mod tests {
126+
use std::collections::BTreeMap;
127+
130128
use stackable_operator::kube::Resource;
131129

132130
use super::build;
@@ -146,8 +144,7 @@ mod tests {
146144
fn build_produces_expected_resource_names() {
147145
let cluster = test_utils::validated_cluster();
148146
let cluster_info = test_utils::cluster_info();
149-
let resources =
150-
build(&cluster, &cluster_info, "hbase-serviceaccount").expect("build succeeds");
147+
let resources = build(&cluster, &cluster_info).expect("build succeeds");
151148

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

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ use stackable_operator::{
1313
k8s_openapi::{
1414
api::{
1515
apps::v1::StatefulSet,
16-
core::v1::{ConfigMap, Service},
16+
core::v1::{ConfigMap, Service, ServiceAccount},
1717
policy::v1::PodDisruptionBudget,
18+
rbac::v1::RoleBinding,
1819
},
1920
apimachinery::pkg::apis::meta::v1::ObjectMeta,
2021
},
@@ -25,6 +26,7 @@ use stackable_operator::{
2526
builder::meta::ownerreference_from_resource,
2627
kvp::label::{recommended_labels, role_group_selector},
2728
role_group_utils::ResourceNames,
29+
role_utils,
2830
types::{
2931
kubernetes::{ConfigMapName, NamespaceName, SecretClassName, Uid},
3032
operator::{
@@ -67,6 +69,8 @@ pub struct KubernetesResources {
6769
pub services: Vec<Service>,
6870
pub config_maps: Vec<ConfigMap>,
6971
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
72+
pub service_accounts: Vec<ServiceAccount>,
73+
pub role_bindings: Vec<RoleBinding>,
7074
}
7175

7276
/// The validated cluster: proves that config merging and validation succeeded for
@@ -132,6 +136,15 @@ impl ValidatedCluster {
132136
RoleName::from_str(&hbase_role.to_string()).expect("an HbaseRole name is a valid role name")
133137
}
134138

139+
/// Type-safe names for the per-cluster RBAC resources: the ServiceAccount shared by all
140+
/// Pods, its (namespaced) RoleBinding, and the operator-deployed ClusterRole it binds.
141+
pub fn rbac_resource_names(&self) -> role_utils::ResourceNames {
142+
role_utils::ResourceNames {
143+
cluster_name: self.name.clone(),
144+
product_name: product_name(),
145+
}
146+
}
147+
135148
/// Type-safe names for the resources of a given role group.
136149
pub(crate) fn resource_names(
137150
&self,
@@ -146,18 +159,33 @@ impl ValidatedCluster {
146159
}
147160

148161
/// Recommended labels for a role-group resource.
149-
pub fn recommended_labels(
162+
pub fn recommended_labels(&self, role: &HbaseRole, role_group_name: &RoleGroupName) -> Labels {
163+
self.recommended_labels_for(&Self::role_name(role), role_group_name)
164+
}
165+
166+
/// Recommended labels for a resource that is not tied to a concrete [`HbaseRole`] (e.g. the
167+
/// Kubernetes executor pod template), using a free-form role/role-group label value.
168+
pub fn recommended_labels_for(
150169
&self,
151-
hbase_role: &HbaseRole,
170+
role_name: &RoleName,
171+
role_group_name: &RoleGroupName,
172+
) -> Labels {
173+
self.recommended_labels_with(&self.product_version, role_name, role_group_name)
174+
}
175+
176+
fn recommended_labels_with(
177+
&self,
178+
product_version: &ProductVersion,
179+
role_name: &RoleName,
152180
role_group_name: &RoleGroupName,
153181
) -> Labels {
154182
recommended_labels(
155183
self,
156184
&product_name(),
157-
&self.product_version,
185+
product_version,
158186
&operator_name(),
159187
&controller_name(),
160-
&Self::role_name(hbase_role),
188+
role_name,
161189
role_group_name,
162190
)
163191
}

0 commit comments

Comments
 (0)