Skip to content

Commit 335c3b3

Browse files
committed
refactor: add infallible rbac functions and add labels
1 parent 8e8f19a commit 335c3b3

6 files changed

Lines changed: 162 additions & 64 deletions

File tree

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::controller::{
1212
config_map::build_rolegroup_config_map,
1313
daemonset::build_server_rolegroup_daemonset,
1414
discovery::build_discovery_config_map,
15+
rbac::{build_role_binding, build_service_account},
1516
service::{
1617
build_rolegroup_headless_service, build_rolegroup_metrics_service,
1718
build_server_role_service,
@@ -51,7 +52,6 @@ pub enum Error {
5152
/// Kubernetes cluster domain used in the discovery URL and the sidecar environment.
5253
pub fn build(
5354
cluster: &ValidatedCluster,
54-
service_account_name: &str,
5555
opa_bundle_builder_image: &str,
5656
user_info_fetcher_image: &str,
5757
cluster_info: &KubernetesClusterInfo,
@@ -81,7 +81,6 @@ pub fn build(
8181
role_group,
8282
opa_bundle_builder_image,
8383
user_info_fetcher_image,
84-
service_account_name,
8584
cluster_info,
8685
)
8786
.context(DaemonSetSnafu {
@@ -98,6 +97,8 @@ pub fn build(
9897
daemon_sets,
9998
services,
10099
config_maps,
100+
service_accounts: vec![build_service_account(cluster)],
101+
role_bindings: vec![build_role_binding(cluster)],
101102
})
102103
}
103104

@@ -115,6 +116,8 @@ stackable_operator::constant!(pub(crate) PLACEHOLDER_DISCOVERY_ROLE_GROUP: RoleG
115116

116117
#[cfg(test)]
117118
mod tests {
119+
use std::collections::BTreeMap;
120+
118121
use serde_json::json;
119122
use stackable_operator::{
120123
commons::networking::DomainName, kube::Resource, utils::cluster_info::KubernetesClusterInfo,
@@ -152,7 +155,6 @@ mod tests {
152155
fn build_produces_expected_resource_names() {
153156
let resources = build(
154157
&cluster(),
155-
"test-opa-serviceaccount",
156158
"bundle-builder-image",
157159
"user-info-fetcher-image",
158160
&cluster_info(),
@@ -179,4 +181,58 @@ mod tests {
179181
["test-opa", "test-opa-server-default"]
180182
);
181183
}
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 resources = build(
191+
&cluster(),
192+
"bundle-builder-image",
193+
"user-info-fetcher-image",
194+
&cluster_info(),
195+
)
196+
.expect("build succeeds");
197+
198+
assert_eq!(
199+
sorted_names(&resources.service_accounts),
200+
["test-opa-serviceaccount"]
201+
);
202+
assert_eq!(
203+
sorted_names(&resources.role_bindings),
204+
["test-opa-rolebinding"]
205+
);
206+
207+
let expected_labels = BTreeMap::from(
208+
[
209+
("app.kubernetes.io/component", "none"),
210+
("app.kubernetes.io/instance", "test-opa"),
211+
(
212+
"app.kubernetes.io/managed-by",
213+
"opa.stackable.tech_opacluster",
214+
),
215+
("app.kubernetes.io/name", "opa"),
216+
("app.kubernetes.io/role-group", "none"),
217+
("app.kubernetes.io/version", "1.2.3-stackable0.0.0-dev"),
218+
("stackable.tech/vendor", "Stackable"),
219+
]
220+
.map(|(key, value)| (key.to_string(), value.to_string())),
221+
);
222+
let service_account = resources
223+
.service_accounts
224+
.first()
225+
.expect("a ServiceAccount is built");
226+
assert_eq!(
227+
service_account.metadata.labels,
228+
Some(expected_labels.clone())
229+
);
230+
231+
let role_binding = resources
232+
.role_bindings
233+
.first()
234+
.expect("a RoleBinding is built");
235+
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
236+
assert_eq!(role_binding.role_ref.name, "opa-clusterrole");
237+
}
182238
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,6 @@ pub fn build_server_rolegroup_daemonset(
244244
role_group: &OpaRoleGroupConfig,
245245
opa_bundle_builder_image: &str,
246246
user_info_fetcher_image: &str,
247-
service_account_name: &str,
248247
cluster_info: &KubernetesClusterInfo,
249248
) -> Result<DaemonSet> {
250249
let resolved_product_image = &cluster.image;
@@ -404,7 +403,12 @@ pub fn build_server_rolegroup_daemonset(
404403
.build(),
405404
)
406405
.context(AddVolumeSnafu)?
407-
.service_account_name(service_account_name)
406+
.service_account_name(
407+
cluster
408+
.rbac_resource_names()
409+
.service_account_name()
410+
.to_string(),
411+
)
408412
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
409413

410414
if let Some(tls) = &cluster.cluster_config.tls {
@@ -870,7 +874,6 @@ mod tests {
870874
role_group,
871875
"bundle-builder-image",
872876
"user-info-fetcher-image",
873-
"test-opa-serviceaccount",
874877
&cluster_info(),
875878
)
876879
.expect("the daemonset should build")

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
pub mod config_map;
55
pub mod daemonset;
66
pub mod discovery;
7+
pub mod rbac;
78
pub mod service;
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/mod.rs

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use stackable_operator::{
1313
},
1414
k8s_openapi::api::{
1515
apps::v1::DaemonSet,
16-
core::v1::{ConfigMap, Service},
16+
core::v1::{ConfigMap, Service, ServiceAccount},
17+
rbac::v1::RoleBinding,
1718
},
1819
kube::{Resource as KubeResource, api::ObjectMeta},
1920
kvp::Labels,
@@ -22,7 +23,7 @@ use stackable_operator::{
2223
HasName, HasUid, NameIsValidLabelValue,
2324
kvp::label::{recommended_labels, role_group_selector, role_selector},
2425
role_group_utils::ResourceNames,
25-
role_utils::{GenericCommonConfig, RoleGroupConfig},
26+
role_utils::{self, GenericCommonConfig, RoleGroupConfig},
2627
types::{
2728
kubernetes::{NamespaceName, Uid},
2829
operator::{
@@ -55,6 +56,9 @@ pub struct ValidatedCluster {
5556
pub name: ClusterName,
5657
pub namespace: NamespaceName,
5758
pub uid: Uid,
59+
/// The product version as a valid label value, for the recommended `app.kubernetes.io/version`
60+
/// label. Derived from the resolved image's app-version label value.
61+
pub product_version: ProductVersion,
5862
pub image: ResolvedProductImage,
5963
pub cluster_config: ValidatedClusterConfig,
6064
pub role_group_configs: BTreeMap<OpaRole, BTreeMap<RoleGroupName, OpaRoleGroupConfig>>,
@@ -69,6 +73,9 @@ impl ValidatedCluster {
6973
cluster_config: ValidatedClusterConfig,
7074
role_group_configs: BTreeMap<OpaRole, BTreeMap<RoleGroupName, OpaRoleGroupConfig>>,
7175
) -> Self {
76+
let product_version = ProductVersion::from_str(&image.app_version_label_value)
77+
.expect("the app version label value is a valid product version");
78+
7279
let metadata = ObjectMeta {
7380
name: Some(name.to_string()),
7481
namespace: Some(namespace.to_string()),
@@ -80,6 +87,7 @@ impl ValidatedCluster {
8087
name,
8188
namespace,
8289
uid,
90+
product_version,
8391
image,
8492
cluster_config,
8593
role_group_configs,
@@ -102,6 +110,15 @@ impl ValidatedCluster {
102110
.expect("the server role name is a valid role name")
103111
}
104112

113+
/// Type-safe names for the per-cluster RBAC resources: the ServiceAccount shared by all
114+
/// Pods, its (namespaced) RoleBinding, and the operator-deployed ClusterRole it binds.
115+
pub fn rbac_resource_names(&self) -> role_utils::ResourceNames {
116+
role_utils::ResourceNames {
117+
cluster_name: self.name.clone(),
118+
product_name: product_name(),
119+
}
120+
}
121+
105122
/// Type-safe names for the resources of a given role group.
106123
pub(crate) fn resource_names(&self, role_group_name: &RoleGroupName) -> ResourceNames {
107124
ResourceNames {
@@ -111,27 +128,33 @@ impl ValidatedCluster {
111128
}
112129
}
113130

114-
/// The product version as a type-safe label value.
115-
///
116-
/// `app_version_label_value` is constructed to be a valid label value, so it is also a valid
117-
/// [`ProductVersion`].
118-
fn product_version(&self) -> ProductVersion {
119-
ProductVersion::from_str(&self.image.app_version_label_value)
120-
.expect("the app version label value is a valid product version")
131+
pub fn recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels {
132+
self.recommended_labels_for(&Self::role_name(), role_group_name)
121133
}
122134

123-
/// Recommended labels for a role-group resource.
124-
///
125-
/// For role-level or cluster-level resources (e.g. the role `Service` or the discovery
126-
/// `ConfigMap`) pass a placeholder role-group name such as `global` or `discovery`.
127-
pub fn recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels {
135+
/// Recommended labels for a resource that is not tied to a concrete role,
136+
/// using a free-form role/role-group label value.
137+
pub fn recommended_labels_for(
138+
&self,
139+
role_name: &RoleName,
140+
role_group_name: &RoleGroupName,
141+
) -> Labels {
142+
self.recommended_labels_with(&self.product_version, role_name, role_group_name)
143+
}
144+
145+
fn recommended_labels_with(
146+
&self,
147+
product_version: &ProductVersion,
148+
role_name: &RoleName,
149+
role_group_name: &RoleGroupName,
150+
) -> Labels {
128151
recommended_labels(
129152
self,
130153
&product_name(),
131-
&self.product_version(),
154+
product_version,
132155
&operator_name(),
133156
&controller_name(),
134-
&Self::role_name(),
157+
role_name,
135158
role_group_name,
136159
)
137160
}
@@ -246,6 +269,8 @@ pub struct KubernetesResources {
246269
pub daemon_sets: Vec<DaemonSet>,
247270
pub services: Vec<Service>,
248271
pub config_maps: Vec<ConfigMap>,
272+
pub service_accounts: Vec<ServiceAccount>,
273+
pub role_bindings: Vec<RoleBinding>,
249274
}
250275

251276
/// Cluster-wide settings resolved once during validation, so the build steps no longer need the

0 commit comments

Comments
 (0)