Skip to content

Commit 3e60002

Browse files
committed
factor: add infallible rbac functions
1 parent 1fc71f1 commit 3e60002

10 files changed

Lines changed: 189 additions & 110 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
@@ -33,5 +33,5 @@ tracing = "0.1"
3333
uuid = "1.23"
3434

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

rust/operator-binary/src/controller.rs

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,17 @@ use snafu::{ResultExt, Snafu};
88
use stackable_operator::{
99
cli::OperatorEnvironmentOptions,
1010
cluster_resources::ClusterResourceApplyStrategy,
11-
commons::rbac::build_rbac_resources,
1211
crd::listener::v1alpha1::Listener,
1312
k8s_openapi::api::{
1413
apps::v1::StatefulSet,
15-
core::v1::{ConfigMap, Service},
14+
core::v1::{ConfigMap, Service, ServiceAccount},
1615
policy::v1::PodDisruptionBudget,
16+
rbac::v1::RoleBinding,
1717
},
1818
kube::{
19-
ResourceExt,
2019
core::{DeserializeGuard, error_boundary},
2120
runtime::controller::Action,
2221
},
23-
kvp::{KeyValuePairError, LabelValueError},
2422
logging::controller::ReconcilerError,
2523
shared::time::Duration,
2624
status::condition::{
@@ -83,6 +81,8 @@ pub struct KubernetesResources {
8381
pub listeners: Vec<Listener>,
8482
pub config_maps: Vec<ConfigMap>,
8583
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
84+
pub service_accounts: Vec<ServiceAccount>,
85+
pub role_bindings: Vec<RoleBinding>,
8686
}
8787

8888
#[derive(Snafu, Debug, EnumDiscriminants)]
@@ -122,26 +122,6 @@ pub enum Error {
122122
source: crate::internal_secret::Error,
123123
},
124124

125-
#[snafu(display("failed to create RBAC service account"))]
126-
ApplyServiceAccount {
127-
source: stackable_operator::cluster_resources::Error,
128-
},
129-
130-
#[snafu(display("failed to create RBAC role binding"))]
131-
ApplyRoleBinding {
132-
source: stackable_operator::cluster_resources::Error,
133-
},
134-
135-
#[snafu(display("failed to build RBAC resources"))]
136-
BuildRbacResources {
137-
source: stackable_operator::commons::rbac::Error,
138-
},
139-
140-
#[snafu(display("failed to get required labels"))]
141-
GetRequiredLabels {
142-
source: KeyValuePairError<LabelValueError>,
143-
},
144-
145125
#[snafu(display("DruidCluster object is invalid"))]
146126
InvalidDruidCluster {
147127
source: error_boundary::InvalidObject,
@@ -196,39 +176,30 @@ pub async fn reconcile_druid(
196176
&druid.spec.object_overrides,
197177
);
198178

199-
let (rbac_sa, rbac_rolebinding) = build_rbac_resources(
200-
druid,
201-
APP_NAME,
202-
cluster_resources
203-
.get_required_labels()
204-
.context(GetRequiredLabelsSnafu)?,
205-
)
206-
.context(BuildRbacResourcesSnafu)?;
207-
208-
// The ServiceAccount name is deterministic on the built object, so the StatefulSet builder only
209-
// needs the name and does not depend on the applied ServiceAccount.
210-
let service_account_name = rbac_sa.name_any();
211-
212-
cluster_resources
213-
.add(client, rbac_sa)
214-
.await
215-
.context(ApplyServiceAccountSnafu)?;
216-
cluster_resources
217-
.add(client, rbac_rolebinding)
218-
.await
219-
.context(ApplyRoleBindingSnafu)?;
220-
221179
// The internal secret is shared across all roles and role groups, so it only needs to be
222180
// created once per reconcile rather than inside the role loop below.
223181
create_shared_internal_secret(&validated_cluster, client, DRUID_CONTROLLER_NAME)
224182
.await
225183
.context(FailedInternalSecretCreationSnafu)?;
226184

227-
let resources =
228-
build::build(&validated_cluster, &service_account_name).context(BuildResourcesSnafu)?;
185+
let resources = build::build(&validated_cluster).context(BuildResourcesSnafu)?;
229186

230187
let mut ss_cond_builder = StatefulSetConditionBuilder::default();
231188

189+
for service_account in resources.service_accounts {
190+
cluster_resources
191+
.add(client, service_account)
192+
.await
193+
.context(ApplyResourceSnafu)?;
194+
}
195+
196+
for role_binding in resources.role_bindings {
197+
cluster_resources
198+
.add(client, role_binding)
199+
.await
200+
.context(ApplyResourceSnafu)?;
201+
}
202+
232203
// Apply order: everything a Pod mounts (ConfigMaps) must exist before the StatefulSets, so the
233204
// StatefulSets are applied last to prevent unnecessary Pod restarts.
234205
// See https://github.com/stackabletech/commons-operator/issues/111 for details.

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

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::str::FromStr;
44

55
use snafu::{ResultExt, Snafu};
6-
use stackable_operator::v2::types::operator::{ProductVersion, RoleGroupName};
6+
use stackable_operator::v2::types::operator::RoleGroupName;
77

88
use crate::{
99
controller::{
@@ -12,6 +12,7 @@ use crate::{
1212
config_map::build_rolegroup_config_map,
1313
listener::{build_group_listener, group_listener_name},
1414
pdb::build_pdb,
15+
rbac::{build_role_binding, build_service_account},
1516
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
1617
statefulset::build_rolegroup_statefulset,
1718
},
@@ -28,10 +29,6 @@ stackable_operator::constant!(pub(crate) PLACEHOLDER_DISCOVERY_ROLE_GROUP: RoleG
2829
// (which is not tied to a single role group).
2930
stackable_operator::constant!(pub(crate) PLACEHOLDER_LISTENER_ROLE_GROUP: RoleGroupName = "none");
3031

31-
// Placeholder product version used for labels on PVC templates, which cannot be modified once
32-
// deployed. A constant value keeps the labels stable across version upgrades.
33-
stackable_operator::constant!(pub(crate) UNVERSIONED_PRODUCT_VERSION: ProductVersion = "none");
34-
3532
pub mod authentication;
3633
pub mod graceful_shutdown;
3734
pub mod jvm;
@@ -66,10 +63,7 @@ pub enum Error {
6663
/// The Router group `Listener` and the discovery `ConfigMap`s are not built here: the discovery
6764
/// `ConfigMap` derives from the *applied* Router listener's ingress address, so both are built and
6865
/// applied in the reconcile step instead.
69-
pub fn build(
70-
cluster: &ValidatedCluster,
71-
service_account_name: &str,
72-
) -> Result<KubernetesResources, Error> {
66+
pub fn build(cluster: &ValidatedCluster) -> Result<KubernetesResources, Error> {
7367
let mut stateful_sets = vec![];
7468
let mut services = vec![];
7569
let mut listeners = vec![];
@@ -116,16 +110,11 @@ pub fn build(
116110
)?,
117111
);
118112
stateful_sets.push(
119-
build_rolegroup_statefulset(
120-
cluster,
121-
druid_role,
122-
role_group_name,
123-
rg,
124-
service_account_name,
125-
)
126-
.context(StatefulSetSnafu {
127-
role_group: role_group_name.clone(),
128-
})?,
113+
build_rolegroup_statefulset(cluster, druid_role, role_group_name, rg).context(
114+
StatefulSetSnafu {
115+
role_group: role_group_name.clone(),
116+
},
117+
)?,
129118
);
130119
}
131120
}
@@ -136,11 +125,15 @@ pub fn build(
136125
listeners,
137126
config_maps,
138127
pod_disruption_budgets,
128+
service_accounts: vec![build_service_account(cluster)],
129+
role_bindings: vec![build_role_binding(cluster)],
139130
})
140131
}
141132

142133
#[cfg(test)]
143134
mod tests {
135+
use std::collections::BTreeMap;
136+
144137
use stackable_operator::kube::Resource;
145138

146139
use super::build;
@@ -161,7 +154,7 @@ mod tests {
161154
fn build_produces_expected_resource_names() {
162155
let druid = druid_from_yaml(MINIMAL_DRUID_YAML);
163156
let cluster = validated_cluster(&druid);
164-
let resources = build(&cluster, "simple-druid-serviceaccount").expect("build succeeds");
157+
let resources = build(&cluster).expect("build succeeds");
165158

166159
// One StatefulSet and one ConfigMap per role group (one role group per role).
167160
let expected_role_group_names = [
@@ -188,4 +181,54 @@ mod tests {
188181
["simple-druid-broker", "simple-druid-coordinator"]
189182
);
190183
}
184+
185+
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
186+
/// accidental drift. The fixture's cluster name deliberately differs from the product name so
187+
/// that swapped `name`/`instance` label values cannot pass unnoticed.
188+
#[test]
189+
fn build_produces_rbac() {
190+
let druid = druid_from_yaml(MINIMAL_DRUID_YAML);
191+
let cluster = validated_cluster(&druid);
192+
let resources = build(&cluster).expect("build succeeds");
193+
194+
assert_eq!(
195+
sorted_names(&resources.service_accounts),
196+
["simple-druid-serviceaccount"]
197+
);
198+
assert_eq!(
199+
sorted_names(&resources.role_bindings),
200+
["simple-druid-rolebinding"]
201+
);
202+
203+
let expected_labels = BTreeMap::from(
204+
[
205+
("app.kubernetes.io/component", "none"),
206+
("app.kubernetes.io/instance", "simple-druid"),
207+
(
208+
"app.kubernetes.io/managed-by",
209+
"druid.stackable.tech_druidcluster",
210+
),
211+
("app.kubernetes.io/name", "druid"),
212+
("app.kubernetes.io/role-group", "none"),
213+
("app.kubernetes.io/version", "30.0.0-stackable0.0.0-dev"),
214+
("stackable.tech/vendor", "Stackable"),
215+
]
216+
.map(|(key, value)| (key.to_string(), value.to_string())),
217+
);
218+
let service_account = resources
219+
.service_accounts
220+
.first()
221+
.expect("a ServiceAccount is built");
222+
assert_eq!(
223+
service_account.metadata.labels,
224+
Some(expected_labels.clone())
225+
);
226+
227+
let role_binding = resources
228+
.role_bindings
229+
.first()
230+
.expect("a RoleBinding is built");
231+
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
232+
assert_eq!(role_binding.role_ref.name, "druid-clusterrole");
233+
}
191234
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ pub mod config_map;
44
pub mod discovery;
55
pub mod listener;
66
pub mod pdb;
7+
pub mod rbac;
78
pub mod service;
89
pub mod statefulset;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub fn build_pdb(
2727
let pdb = pod_disruption_budget_builder_with_role(
2828
cluster,
2929
&product_name(),
30-
&role.to_role_name(),
30+
&ValidatedCluster::role_name(role),
3131
&operator_name(),
3232
&controller_name(),
3333
)
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::validate::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 all 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+
}

0 commit comments

Comments
 (0)