Skip to content

Commit 08c1e18

Browse files
factor: add infallible rbac functions (#1251)
* factor: add infallible rbac functions * changelog --------- Co-authored-by: Siegfried Weber <mail@siegfriedweber.net>
1 parent d69cbcc commit 08c1e18

3 files changed

Lines changed: 271 additions & 0 deletions

File tree

crates/stackable-operator/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ All notable changes to this project will be documented in this file.
77
### Added
88

99
- [v2] Add `EnvVarSet::with_env_var` to add a given `EnvVar` to the set ([#1249]).
10+
- [v2] Add `rbac::build_service_account` and `rbac::build_role_binding`, the infallible variant of
11+
`commons::rbac::build_rbac_resources` based on typed names and owner references ([#1251]).
1012

1113
### Changed
1214

1315
- [v2] BREAKING: Converting an `EnvVarSet` into a `Vec<EnvVar>` takes dependencies between
1416
environment variables into account ([#1249]).
1517

1618
[#1249]: https://github.com/stackabletech/operator-rs/pull/1249
19+
[#1251]: https://github.com/stackabletech/operator-rs/pull/1251
1720

1821
## [0.113.4] - 2026-07-09
1922

crates/stackable-operator/src/v2/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod jvm_argument_overrides;
1010
pub mod kvp;
1111
pub mod macros;
1212
pub mod product_logging;
13+
pub mod rbac;
1314
pub mod role_group_utils;
1415
pub mod role_utils;
1516
pub mod types;
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
use crate::{
2+
builder::meta::ObjectMetaBuilder,
3+
k8s_openapi::{
4+
Resource as KubeApiResource,
5+
api::{
6+
core::v1::ServiceAccount,
7+
rbac::v1::{ClusterRole, RoleBinding, RoleRef, Subject},
8+
},
9+
apimachinery::pkg::apis::meta::v1::ObjectMeta,
10+
},
11+
kube::{Resource, ResourceExt},
12+
kvp::Labels,
13+
v2::{HasName, HasUid, builder::meta::ownerreference_from_resource, role_utils::ResourceNames},
14+
};
15+
16+
/// Builds the [`ServiceAccount`] for the product workloads, named
17+
/// `<cluster_name>-serviceaccount` after the given [`ResourceNames`].
18+
///
19+
/// Together with [`build_role_binding`] this is the infallible variant of
20+
/// [`crate::commons::rbac::build_rbac_resources`]; the output is identical as long as
21+
/// `resource_names.cluster_name` matches the metadata name of `owner`.
22+
pub fn build_service_account(
23+
owner: &(impl Resource<DynamicType = ()> + HasName + HasUid),
24+
resource_names: &ResourceNames,
25+
labels: Labels,
26+
) -> ServiceAccount {
27+
ServiceAccount {
28+
metadata: build_metadata(
29+
owner,
30+
resource_names.service_account_name().to_string(),
31+
labels,
32+
),
33+
..ServiceAccount::default()
34+
}
35+
}
36+
37+
/// Builds the [`RoleBinding`] for the product workloads, named `<cluster_name>-rolebinding`
38+
/// after the given [`ResourceNames`]. It binds the [`ServiceAccount`] from
39+
/// [`build_service_account`] to the operator-deployed ClusterRole
40+
/// `<product_name>-clusterrole`, which must already exist.
41+
///
42+
/// Together with [`build_service_account`] this is the infallible variant of
43+
/// [`crate::commons::rbac::build_rbac_resources`]; the output is identical as long as
44+
/// `resource_names.cluster_name` matches the metadata name of `owner`.
45+
pub fn build_role_binding(
46+
owner: &(impl Resource<DynamicType = ()> + HasName + HasUid),
47+
resource_names: &ResourceNames,
48+
labels: Labels,
49+
) -> RoleBinding {
50+
RoleBinding {
51+
metadata: build_metadata(
52+
owner,
53+
resource_names.role_binding_name().to_string(),
54+
labels,
55+
),
56+
role_ref: RoleRef {
57+
api_group: Some(ClusterRole::GROUP.to_owned()),
58+
kind: ClusterRole::KIND.to_owned(),
59+
name: resource_names.cluster_role_name().to_string(),
60+
},
61+
subjects: Some(vec![Subject {
62+
kind: ServiceAccount::KIND.to_owned(),
63+
name: resource_names.service_account_name().to_string(),
64+
namespace: owner.namespace(),
65+
// Left unset because the ServiceAccount kind is in the core API group.
66+
api_group: None,
67+
}]),
68+
}
69+
}
70+
71+
/// Common metadata of the RBAC resources: name, the owner's namespace, an owner reference on
72+
/// `owner` and the given labels.
73+
fn build_metadata(
74+
owner: &(impl Resource<DynamicType = ()> + HasName + HasUid),
75+
name: impl Into<String>,
76+
labels: Labels,
77+
) -> ObjectMeta {
78+
ObjectMetaBuilder::new()
79+
.name(name)
80+
.namespace_opt(owner.namespace())
81+
.ownerreference(ownerreference_from_resource(owner, None, Some(true)))
82+
.with_labels(labels)
83+
.build()
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use std::borrow::Cow;
89+
90+
use crate::{
91+
k8s_openapi::{
92+
api::{
93+
core::v1::ServiceAccount,
94+
rbac::v1::{RoleBinding, RoleRef, Subject},
95+
},
96+
apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference},
97+
},
98+
kube::Resource,
99+
kvp::Labels,
100+
v2::{
101+
HasName, HasUid,
102+
rbac::{build_role_binding, build_service_account},
103+
role_utils::ResourceNames,
104+
types::{
105+
kubernetes::Uid,
106+
operator::{ClusterName, ProductName},
107+
},
108+
},
109+
};
110+
111+
#[derive(Clone)]
112+
struct Cluster {
113+
object_meta: ObjectMeta,
114+
}
115+
116+
impl Cluster {
117+
fn new() -> Self {
118+
Self {
119+
object_meta: ObjectMeta {
120+
name: Some("cluster-name".to_owned()),
121+
namespace: Some("cluster-namespace".to_owned()),
122+
uid: Some("a6b89911-d48e-4328-88d6-b9251226583d".to_owned()),
123+
..ObjectMeta::default()
124+
},
125+
}
126+
}
127+
}
128+
129+
impl Resource for Cluster {
130+
type DynamicType = ();
131+
type Scope = ();
132+
133+
fn kind(_dt: &Self::DynamicType) -> Cow<'_, str> {
134+
Cow::from("kind")
135+
}
136+
137+
fn group(_dt: &Self::DynamicType) -> Cow<'_, str> {
138+
Cow::from("group")
139+
}
140+
141+
fn version(_dt: &Self::DynamicType) -> Cow<'_, str> {
142+
Cow::from("version")
143+
}
144+
145+
fn plural(_dt: &Self::DynamicType) -> Cow<'_, str> {
146+
Cow::from("plural")
147+
}
148+
149+
fn meta(&self) -> &ObjectMeta {
150+
&self.object_meta
151+
}
152+
153+
fn meta_mut(&mut self) -> &mut ObjectMeta {
154+
&mut self.object_meta
155+
}
156+
}
157+
158+
impl HasName for Cluster {
159+
fn to_name(&self) -> String {
160+
self.object_meta
161+
.name
162+
.clone()
163+
.expect("should be set in Cluster::new")
164+
}
165+
}
166+
167+
impl HasUid for Cluster {
168+
fn to_uid(&self) -> Uid {
169+
Uid::from_str_unsafe(
170+
&self
171+
.object_meta
172+
.uid
173+
.clone()
174+
.expect("should be set in Cluster::new"),
175+
)
176+
}
177+
}
178+
179+
fn resource_names() -> ResourceNames {
180+
ResourceNames {
181+
cluster_name: ClusterName::from_str_unsafe("cluster-name"),
182+
product_name: ProductName::from_str_unsafe("my-product"),
183+
}
184+
}
185+
186+
fn labels() -> Labels {
187+
Labels::common("my-product", "cluster-name").expect("should be valid label values")
188+
}
189+
190+
fn expected_metadata(name: &str) -> ObjectMeta {
191+
ObjectMeta {
192+
labels: Some(
193+
[
194+
("app.kubernetes.io/instance", "cluster-name"),
195+
("app.kubernetes.io/name", "my-product"),
196+
]
197+
.map(|(key, value)| (key.to_owned(), value.to_owned()))
198+
.into(),
199+
),
200+
name: Some(name.to_owned()),
201+
namespace: Some("cluster-namespace".to_owned()),
202+
owner_references: Some(vec![OwnerReference {
203+
api_version: "group/version".to_owned(),
204+
controller: Some(true),
205+
kind: "kind".to_owned(),
206+
name: "cluster-name".to_owned(),
207+
uid: "a6b89911-d48e-4328-88d6-b9251226583d".to_owned(),
208+
..OwnerReference::default()
209+
}]),
210+
..ObjectMeta::default()
211+
}
212+
}
213+
214+
#[test]
215+
fn build_expected_service_account() {
216+
let expected_service_account = ServiceAccount {
217+
metadata: expected_metadata("cluster-name-serviceaccount"),
218+
..ServiceAccount::default()
219+
};
220+
221+
assert_eq!(
222+
expected_service_account,
223+
build_service_account(&Cluster::new(), &resource_names(), labels())
224+
);
225+
}
226+
227+
#[test]
228+
fn build_expected_role_binding() {
229+
let expected_role_binding = RoleBinding {
230+
metadata: expected_metadata("cluster-name-rolebinding"),
231+
role_ref: RoleRef {
232+
api_group: Some("rbac.authorization.k8s.io".to_owned()),
233+
kind: "ClusterRole".to_owned(),
234+
name: "my-product-clusterrole".to_owned(),
235+
},
236+
subjects: Some(vec![Subject {
237+
kind: "ServiceAccount".to_owned(),
238+
name: "cluster-name-serviceaccount".to_owned(),
239+
namespace: Some("cluster-namespace".to_owned()),
240+
api_group: None,
241+
}]),
242+
};
243+
244+
assert_eq!(
245+
expected_role_binding,
246+
build_role_binding(&Cluster::new(), &resource_names(), labels())
247+
);
248+
}
249+
250+
#[test]
251+
fn output_is_identical_to_v1_build_rbac_resources() {
252+
let cluster = Cluster::new();
253+
254+
let (v1_service_account, v1_role_binding) =
255+
crate::commons::rbac::build_rbac_resources(&cluster, "my-product", labels())
256+
.expect("should build the v1 RBAC resources");
257+
258+
assert_eq!(
259+
v1_service_account,
260+
build_service_account(&cluster, &resource_names(), labels())
261+
);
262+
assert_eq!(
263+
v1_role_binding,
264+
build_role_binding(&cluster, &resource_names(), labels())
265+
);
266+
}
267+
}

0 commit comments

Comments
 (0)