Skip to content

Commit abd02d1

Browse files
committed
factor: add infallible rbac functions
1 parent 1d7bf05 commit abd02d1

9 files changed

Lines changed: 182 additions & 102 deletions

File tree

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();

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

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ use stackable_operator::{
1010
crd::listener::v1alpha1::Listener,
1111
k8s_openapi::api::{
1212
apps::v1::StatefulSet,
13-
core::v1::{ConfigMap, Service},
13+
core::v1::{ConfigMap, Service, ServiceAccount},
1414
policy::v1::PodDisruptionBudget,
15+
rbac::v1::RoleBinding,
1516
},
1617
kube::{Resource, api::ObjectMeta},
1718
kvp::Labels,
@@ -22,6 +23,7 @@ use stackable_operator::{
2223
builder::meta::ownerreference_from_resource,
2324
kvp::label::{recommended_labels, role_group_selector},
2425
role_group_utils::ResourceNames,
26+
role_utils,
2527
types::{
2628
kubernetes::{ListenerClassName, NamespaceName, SecretClassName, Uid},
2729
operator::{
@@ -64,13 +66,18 @@ pub(crate) fn shared_spooling_secret_name(cluster_name: &ClusterName) -> String
6466
format!("{cluster_name}-spooling-secret")
6567
}
6668

69+
// Placeholder version label value for resources whose labels must not change after deployment.
70+
stackable_operator::constant!(UNVERSIONED_PRODUCT_VERSION: ProductVersion = "none");
71+
6772
/// Every Kubernetes resource produced by the client-free [`build()`](build::build) step.
6873
pub struct KubernetesResources {
6974
pub stateful_sets: Vec<StatefulSet>,
7075
pub services: Vec<Service>,
7176
pub listeners: Vec<Listener>,
7277
pub config_maps: Vec<ConfigMap>,
7378
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
79+
pub service_accounts: Vec<ServiceAccount>,
80+
pub role_bindings: Vec<RoleBinding>,
7481
}
7582

7683
#[derive(Clone, Debug)]
@@ -153,7 +160,11 @@ pub struct ValidatedCluster {
153160
pub namespace: NamespaceName,
154161
pub uid: Uid,
155162
pub image: ResolvedProductImage,
156-
pub product_version: u16,
163+
/// The numeric Trino version (e.g. `481`), used for version-dependent configuration.
164+
pub numeric_product_version: u16,
165+
/// The version label value (`app.kubernetes.io/version`) as a type-safe [`ProductVersion`],
166+
/// parsed once from the resolved image's app version label value.
167+
pub product_version: ProductVersion,
157168
pub cluster_config: ValidatedClusterConfig,
158169
pub role_configs: BTreeMap<TrinoRole, ValidatedRoleConfig>,
159170
pub role_group_configs: BTreeMap<TrinoRole, BTreeMap<RoleGroupName, TrinoRoleGroupConfig>>,
@@ -166,7 +177,7 @@ impl ValidatedCluster {
166177
namespace: NamespaceName,
167178
uid: Uid,
168179
image: ResolvedProductImage,
169-
product_version: u16,
180+
numeric_product_version: u16,
170181
cluster_config: ValidatedClusterConfig,
171182
role_configs: BTreeMap<TrinoRole, ValidatedRoleConfig>,
172183
role_group_configs: BTreeMap<TrinoRole, BTreeMap<RoleGroupName, TrinoRoleGroupConfig>>,
@@ -181,8 +192,10 @@ impl ValidatedCluster {
181192
name,
182193
namespace,
183194
uid,
195+
product_version: ProductVersion::from_str(&image.app_version_label_value)
196+
.expect("the app version label value is a valid product version"),
184197
image,
185-
product_version,
198+
numeric_product_version,
186199
cluster_config,
187200
role_configs,
188201
role_group_configs,
@@ -219,6 +232,15 @@ impl ValidatedCluster {
219232
self.cluster_config.authentication_enabled() || self.server_tls_enabled()
220233
}
221234

235+
/// Type-safe names for the per-cluster RBAC resources: the ServiceAccount shared by all
236+
/// Pods, its (namespaced) RoleBinding, and the operator-deployed ClusterRole it binds.
237+
pub fn rbac_resource_names(&self) -> role_utils::ResourceNames {
238+
role_utils::ResourceNames {
239+
cluster_name: self.name.clone(),
240+
product_name: product_name(),
241+
}
242+
}
243+
222244
/// Type-safe names for the resources of a given role group.
223245
pub(crate) fn resource_names(
224246
&self,
@@ -270,16 +292,10 @@ impl ValidatedCluster {
270292
RoleName::from_str(&role.to_string()).expect("a TrinoRole is a valid RFC 1123 role name")
271293
}
272294

273-
/// The version label value (`app.kubernetes.io/version`) as a type-safe [`ProductVersion`].
274-
fn version_label(&self) -> ProductVersion {
275-
ProductVersion::from_str(&self.image.app_version_label_value)
276-
.expect("the app version label value is a valid product version")
277-
}
278-
279-
fn recommended_labels_with_version(
295+
fn recommended_labels_with(
280296
&self,
281297
version: &ProductVersion,
282-
role: &TrinoRole,
298+
role_name: &RoleName,
283299
role_group_name: &RoleGroupName,
284300
) -> Labels {
285301
recommended_labels(
@@ -288,38 +304,42 @@ impl ValidatedCluster {
288304
version,
289305
&operator_name(),
290306
&controller_name(),
291-
&Self::role_name(role),
307+
role_name,
292308
role_group_name,
293309
)
294310
}
295311

296312
/// Recommended labels for a role-group resource (using the resolved product version).
297-
pub(crate) fn recommended_labels(
313+
pub fn recommended_labels(&self, role: &TrinoRole, role_group_name: &RoleGroupName) -> Labels {
314+
self.recommended_labels_for(&Self::role_name(role), role_group_name)
315+
}
316+
317+
/// Recommended labels for a resource that is not tied to a concrete [`TrinoRole`] (e.g. the
318+
/// cluster-shared RBAC resources), using a free-form role/role-group label value.
319+
pub fn recommended_labels_for(
298320
&self,
299-
role: &TrinoRole,
321+
role_name: &RoleName,
300322
role_group_name: &RoleGroupName,
301323
) -> Labels {
302-
self.recommended_labels_with_version(&self.version_label(), role, role_group_name)
324+
self.recommended_labels_with(&self.product_version, role_name, role_group_name)
303325
}
304326

305-
/// Recommended labels using a fixed `"none"` version, for resources whose labels must not
306-
/// change after creation (e.g. listener PVC templates).
307-
pub(crate) fn unversioned_recommended_labels(
327+
/// Recommended labels with the constant [`UNVERSIONED_PRODUCT_VERSION`], for resources whose
328+
/// labels must not change after creation (e.g. listener PVC templates).
329+
pub fn unversioned_recommended_labels(
308330
&self,
309331
role: &TrinoRole,
310332
role_group_name: &RoleGroupName,
311333
) -> Labels {
312-
let none = ProductVersion::from_str("none")
313-
.expect("\"none\" is a valid product version label value");
314-
self.recommended_labels_with_version(&none, role, role_group_name)
334+
self.recommended_labels_with(
335+
&UNVERSIONED_PRODUCT_VERSION,
336+
&Self::role_name(role),
337+
role_group_name,
338+
)
315339
}
316340

317341
/// Selector labels matching the pods of a role group.
318-
pub(crate) fn role_group_selector(
319-
&self,
320-
role: &TrinoRole,
321-
role_group_name: &RoleGroupName,
322-
) -> Labels {
342+
pub fn role_group_selector(&self, role: &TrinoRole, role_group_name: &RoleGroupName) -> Labels {
323343
role_group_selector(
324344
self,
325345
&product_name(),

0 commit comments

Comments
 (0)