Skip to content

Commit 47db5b6

Browse files
committed
factor: add infallible rbac functions
1 parent 86d70aa commit 47db5b6

10 files changed

Lines changed: 183 additions & 112 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
@@ -30,4 +30,4 @@ tracing = "0.1"
3030

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

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

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::controller::{
1313
config_map,
1414
listener::{build_group_listener, group_listener_name},
1515
pdb::build_pdb,
16+
rbac::{build_role_binding, build_service_account},
1617
service::{
1718
build_rolegroup_headless_service, build_rolegroup_metrics_service,
1819
headless_service_ports,
@@ -52,13 +53,9 @@ pub enum Error {
5253
/// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
5354
/// dereferenced and validated by this point, so the errors returned here are resource-assembly
5455
/// failures only.
55-
///
56-
/// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under
57-
/// (RBAC resources are built and applied separately, in the reconcile step).
5856
pub fn build(
5957
cluster: &ValidatedCluster,
6058
cluster_info: &KubernetesClusterInfo,
61-
service_account_name: &str,
6259
) -> Result<KubernetesResources, Error> {
6360
let mut stateful_sets = vec![];
6461
let mut services = vec![];
@@ -115,7 +112,6 @@ pub fn build(
115112
role,
116113
role_group_name,
117114
role_group_config,
118-
service_account_name,
119115
)
120116
.context(StatefulSetSnafu {
121117
role_group: role_group_name.clone(),
@@ -147,11 +143,15 @@ pub fn build(
147143
listeners,
148144
config_maps,
149145
pod_disruption_budgets,
146+
service_accounts: vec![build_service_account(cluster)],
147+
role_bindings: vec![build_role_binding(cluster)],
150148
})
151149
}
152150

153151
#[cfg(test)]
154152
mod tests {
153+
use std::collections::BTreeMap;
154+
155155
use stackable_operator::{
156156
commons::networking::DomainName, kube::Resource, utils::cluster_info::KubernetesClusterInfo,
157157
};
@@ -177,8 +177,7 @@ mod tests {
177177
.expect("cluster.local is a valid domain name"),
178178
};
179179

180-
let resources =
181-
build(&cluster, &cluster_info, "simple-trino-serviceaccount").expect("build succeeds");
180+
let resources = build(&cluster, &cluster_info).expect("build succeeds");
182181

183182
// One StatefulSet per role group.
184183
assert_eq!(
@@ -219,4 +218,58 @@ mod tests {
219218
["simple-trino-coordinator", "simple-trino-worker"]
220219
);
221220
}
221+
222+
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
223+
/// accidental drift. The fixture's cluster name deliberately differs from the product name so
224+
/// that swapped `name`/`instance` label values cannot pass unnoticed.
225+
#[test]
226+
fn build_produces_rbac() {
227+
let cluster = validated_cluster();
228+
let cluster_info = KubernetesClusterInfo {
229+
cluster_domain: DomainName::try_from("cluster.local")
230+
.expect("cluster.local is a valid domain name"),
231+
};
232+
233+
let resources = build(&cluster, &cluster_info).expect("build succeeds");
234+
235+
assert_eq!(
236+
sorted_names(&resources.service_accounts),
237+
["simple-trino-serviceaccount"]
238+
);
239+
assert_eq!(
240+
sorted_names(&resources.role_bindings),
241+
["simple-trino-rolebinding"]
242+
);
243+
244+
let expected_labels = BTreeMap::from(
245+
[
246+
("app.kubernetes.io/component", "none"),
247+
("app.kubernetes.io/instance", "simple-trino"),
248+
(
249+
"app.kubernetes.io/managed-by",
250+
"trino.stackable.tech_trinocluster",
251+
),
252+
("app.kubernetes.io/name", "trino"),
253+
("app.kubernetes.io/role-group", "none"),
254+
("app.kubernetes.io/version", "481-stackable0.0.0-dev"),
255+
("stackable.tech/vendor", "Stackable"),
256+
]
257+
.map(|(key, value)| (key.to_string(), value.to_string())),
258+
);
259+
let service_account = resources
260+
.service_accounts
261+
.first()
262+
.expect("a ServiceAccount is built");
263+
assert_eq!(
264+
service_account.metadata.labels,
265+
Some(expected_labels.clone())
266+
);
267+
268+
let role_binding = resources
269+
.role_bindings
270+
.first()
271+
.expect("a RoleBinding is built");
272+
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
273+
assert_eq!(role_binding.role_ref.name, "trino-clusterrole");
274+
}
222275
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ pub fn build_rolegroup_config_map(
164164
// 8. jvm.config. The role + role-group `jvmArgumentOverrides` were already merged in the
165165
// validate step and are carried by `product_specific_common_config`.
166166
let jvm_config = jvm::jvm_config(
167-
cluster.product_version,
167+
cluster.numeric_product_version,
168168
&rg.config,
169169
&rg.product_specific_common_config.jvm_argument_overrides,
170170
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
pub mod config_map;
88
pub mod listener;
99
pub mod pdb;
10+
pub mod rbac;
1011
pub mod service;
1112
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 coordinator and worker 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: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ use crate::{
4141
authorization::opa::OPA_TLS_VOLUME_NAME,
4242
controller::{
4343
MAX_PREPARE_LOG_FILE_SIZE, RoleGroupName, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR,
44-
TrinoRoleGroupConfig, ValidatedCluster, build,
44+
TrinoRoleGroupConfig, ValidatedCluster,
4545
build::{
46-
command,
46+
self, command,
4747
resource::listener::{
4848
LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build_group_listener_pvc,
4949
group_listener_name, secret_volume_listener_scope,
@@ -137,10 +137,9 @@ pub fn build_rolegroup_statefulset(
137137
trino_role: &TrinoRole,
138138
role_group_name: &RoleGroupName,
139139
role_group_config: &TrinoRoleGroupConfig,
140-
sa_name: &str,
141140
) -> Result<StatefulSet> {
142141
// Everything below is derived from the validated cluster and the validated role-group config,
143-
// so the caller only needs to pass those (plus the applied ServiceAccount name).
142+
// so the caller only needs to pass those.
144143
let resolved_product_image = &cluster.image;
145144
let trino_authentication_config = &cluster.cluster_config.authentication;
146145
let catalogs = &cluster.cluster_config.catalogs;
@@ -422,7 +421,12 @@ pub fn build_rolegroup_statefulset(
422421
)),
423422
)
424423
.context(AddVolumeSnafu)?
425-
.service_account_name(sa_name)
424+
.service_account_name(
425+
cluster
426+
.rbac_resource_names()
427+
.service_account_name()
428+
.to_string(),
429+
)
426430
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
427431

428432
let mut pod_template = pod_builder.build_template();

0 commit comments

Comments
 (0)