Skip to content

Commit 8ff2aea

Browse files
committed
refactor connect server service account and role binding
1 parent cfd6806 commit 8ff2aea

6 files changed

Lines changed: 105 additions & 78 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ indoc = "2"
3131
regex = "1.12"
3232

3333
[patch."https://github.com/stackabletech/operator-rs.git"]
34-
# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch="main" }
34+
stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch="feat/smooth-operator/build-rbac" }
3535
# stackable-operator = { path = "../operator-rs/crates/stackable-operator" }

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

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::sync::Arc;
33
use snafu::{ResultExt, Snafu};
44
use stackable_operator::{
55
cluster_resources::ClusterResourceApplyStrategy,
6-
commons::rbac::build_rbac_resources,
76
crd::listener,
87
k8s_openapi::api::{
98
apps::v1::StatefulSet,
@@ -25,7 +24,7 @@ use stackable_operator::{
2524
};
2625
use strum::{EnumDiscriminants, IntoStaticStr};
2726

28-
use super::crd::{CONNECT_APP_NAME, v1alpha1};
27+
use super::crd::v1alpha1;
2928
use crate::{Ctx, connect::crd::SparkConnectServerStatus, crd::constants::OPERATOR_NAME};
3029

3130
pub mod build;
@@ -44,16 +43,6 @@ pub enum Error {
4443
source: stackable_operator::cluster_resources::Error,
4544
},
4645

47-
#[snafu(display("failed to apply role ServiceAccount"))]
48-
ApplyServiceAccount {
49-
source: stackable_operator::cluster_resources::Error,
50-
},
51-
52-
#[snafu(display("failed to apply global RoleBinding"))]
53-
ApplyRoleBinding {
54-
source: stackable_operator::cluster_resources::Error,
55-
},
56-
5746
#[snafu(display("failed to update status of spark connect server {name}"))]
5847
ApplyStatus {
5948
source: stackable_operator::client::Error,
@@ -150,32 +139,19 @@ pub async fn reconcile(
150139
&scs.spec.object_overrides,
151140
);
152141

153-
// Use a dedicated service account for connect server pods. Building the RBAC resources needs
154-
// the cluster-resource labels, so it stays in the reconcile step; the built objects (whose
155-
// names are deterministic) are handed to the client-free build step.
156-
let (service_account, role_binding) = build_rbac_resources(
157-
scs,
158-
CONNECT_APP_NAME,
159-
cluster_resources
160-
.get_required_labels()
161-
.context(GetRequiredLabelsSnafu)?,
162-
)
163-
.context(BuildRbacResourcesSnafu)?;
164-
165-
let resources = build::build(&validated, service_account, role_binding, &scs.spec.args)
166-
.context(BuildResourcesSnafu)?;
142+
let resources = build::build(&validated, &scs.spec.args).context(BuildResourcesSnafu)?;
167143

168144
// Apply order: ServiceAccount and RoleBinding first, then the Services, ConfigMaps and
169145
// Listener, and finally the StatefulSet (it mounts the ConfigMaps and runs under the SA, so
170146
// they must exist first).
171147
cluster_resources
172148
.add(client, resources.service_account)
173149
.await
174-
.context(ApplyServiceAccountSnafu)?;
150+
.context(ApplyResourceSnafu)?;
175151
cluster_resources
176152
.add(client, resources.role_binding)
177153
.await
178-
.context(ApplyRoleBindingSnafu)?;
154+
.context(ApplyResourceSnafu)?;
179155
for service in resources.services {
180156
cluster_resources
181157
.add(client, service)

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

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@
66
//! together in one module is clearer than scattering them across per-kind modules.
77
88
pub(crate) mod executor;
9+
pub(crate) mod rbac;
910
pub(crate) mod server;
1011
pub(crate) mod service;
1112

1213
use snafu::{ResultExt, Snafu};
13-
use stackable_operator::{
14-
k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding},
15-
kube::ResourceExt,
16-
};
14+
use stackable_operator::kube::ResourceExt;
1715

1816
use crate::connect::{
1917
common,
20-
controller::{SparkConnectResources, validate::ValidatedSparkConnectServer},
18+
controller::{
19+
SparkConnectResources,
20+
build::rbac::{build_role_binding, build_service_account},
21+
validate::ValidatedSparkConnectServer,
22+
},
2123
};
2224

2325
#[derive(Snafu, Debug)]
@@ -56,8 +58,6 @@ pub enum Error {
5658
/// Builds every Kubernetes resource for the given validated SparkConnectServer.
5759
pub(crate) fn build(
5860
validated: &ValidatedSparkConnectServer,
59-
service_account: ServiceAccount,
60-
role_binding: RoleBinding,
6161
user_args: &[String],
6262
) -> Result<SparkConnectResources, Error> {
6363
let resolved_s3 = &validated.cluster_config.resolved_s3;
@@ -70,8 +70,7 @@ pub(crate) fn build(
7070
resolved_s3
7171
.spark_properties()
7272
.context(S3SparkPropertiesSnafu)?,
73-
server::server_properties(validated, &headless_service, &service_account)
74-
.context(ServerPropertiesSnafu)?,
73+
server::server_properties(validated, &headless_service).context(ServerPropertiesSnafu)?,
7574
executor::executor_properties(validated).context(ExecutorPropertiesSnafu)?,
7675
])
7776
.context(SerializePropertiesSnafu)?;
@@ -97,18 +96,13 @@ pub(crate) fn build(
9796
let listener = server::build_listener(validated);
9897

9998
let args = server::command_args(user_args);
100-
let stateful_set = server::build_stateful_set(
101-
validated,
102-
&service_account,
103-
&server_config_map,
104-
&listener.name_any(),
105-
args,
106-
)
107-
.context(BuildServerStatefulSetSnafu)?;
99+
let stateful_set =
100+
server::build_stateful_set(validated, &server_config_map, &listener.name_any(), args)
101+
.context(BuildServerStatefulSetSnafu)?;
108102

109103
Ok(SparkConnectResources {
110-
service_account,
111-
role_binding,
104+
service_account: build_service_account(validated),
105+
role_binding: build_role_binding(validated),
112106
services: vec![headless_service, metrics_service],
113107
config_maps: vec![executor_config_map, server_config_map],
114108
listener,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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::connect::controller::validate::ValidatedSparkConnectServer;
15+
16+
stackable_operator::constant!(NONE_ROLE_NAME: RoleName = "none");
17+
stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none");
18+
19+
pub fn build_service_account(server: &ValidatedSparkConnectServer) -> ServiceAccount {
20+
rbac::build_service_account(server, &server.rbac_resource_names(), rbac_labels(server))
21+
}
22+
23+
pub fn build_role_binding(server: &ValidatedSparkConnectServer) -> RoleBinding {
24+
rbac::build_role_binding(server, &server.rbac_resource_names(), rbac_labels(server))
25+
}
26+
27+
fn rbac_labels(server: &ValidatedSparkConnectServer) -> Labels {
28+
server.recommended_labels_for(&NONE_ROLE_NAME, &NONE_ROLE_GROUP_NAME)
29+
}

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ use stackable_operator::{
2323
DeepMerge,
2424
api::{
2525
apps::v1::{StatefulSet, StatefulSetSpec},
26-
core::v1::{
27-
ConfigMap, EnvVar, HTTPGetAction, PodSecurityContext, Probe, Service,
28-
ServiceAccount,
29-
},
26+
core::v1::{ConfigMap, EnvVar, HTTPGetAction, PodSecurityContext, Probe, Service},
3027
},
3128
apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
3229
},
@@ -176,7 +173,6 @@ pub(crate) fn server_config_map(
176173

177174
pub(crate) fn build_stateful_set(
178175
validated: &ValidatedSparkConnectServer,
179-
service_account: &ServiceAccount,
180176
config_map: &ConfigMap,
181177
listener_name: &str,
182178
args: Vec<String>,
@@ -194,7 +190,7 @@ pub(crate) fn build_stateful_set(
194190

195191
let mut pb = PodBuilder::new();
196192

197-
pb.service_account_name(service_account.name_unchecked())
193+
pb.service_account_name(validated.rbac_resource_names().service_account_name())
198194
.metadata(metadata)
199195
.image_pull_secrets_from_product_image(resolved_product_image)
200196
.add_volume(
@@ -402,13 +398,12 @@ fn env(env_overrides: Option<&HashMap<String, String>>) -> Result<Vec<EnvVar>, E
402398
pub(crate) fn server_properties(
403399
validated: &ValidatedSparkConnectServer,
404400
driver_service: &Service,
405-
service_account: &ServiceAccount,
406401
) -> Result<BTreeMap<String, Option<String>>, Error> {
407402
let config = &validated.server_config;
408403
let resolved_product_image = &validated.resolved_product_image;
409404
let spark_image = resolved_product_image.image.clone();
410405
let spark_version = resolved_product_image.product_version.clone();
411-
let service_account_name = service_account.name_unchecked();
406+
let service_account_name = validated.rbac_resource_names().service_account_name();
412407
let namespace = driver_service
413408
.namespace()
414409
.context(ObjectHasNoNamespaceSnafu)?;
@@ -434,7 +429,7 @@ pub(crate) fn server_properties(
434429
("spark.kubernetes.namespace".to_string(), Some(namespace)),
435430
(
436431
"spark.kubernetes.authenticate.driver.serviceAccountName".to_string(),
437-
Some(service_account_name),
432+
Some(service_account_name.to_string()),
438433
),
439434
(
440435
"spark.kubernetes.driver.pod.name".to_string(),

rust/operator-binary/src/connect/controller/validate.rs

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use stackable_operator::{
2222
product_logging::framework::{
2323
VectorContainerLogConfig, validate_logging_configuration_for_container,
2424
},
25-
role_utils::JavaCommonConfig,
25+
role_utils::{self, JavaCommonConfig},
2626
types::{
2727
kubernetes::{ConfigMapName, ListenerClassName, NamespaceName, Uid},
2828
operator::{
@@ -118,6 +118,10 @@ fn validate_logging(
118118

119119
type Result<T, E = Error> = std::result::Result<T, E>;
120120

121+
stackable_operator::constant!(
122+
DEFAULT_SPARK_CONNECT_ROLE_GROUP: RoleGroupName = DEFAULT_SPARK_CONNECT_GROUP_NAME
123+
);
124+
121125
/// Validated logging configuration for the (optional) Vector container.
122126
///
123127
/// Produced up-front by [`validate_logging`] so that an
@@ -134,6 +138,7 @@ pub struct ValidatedSparkConnectServer {
134138
pub name: ClusterName,
135139
pub namespace: NamespaceName,
136140
pub uid: Uid,
141+
pub product_version: ProductVersion,
137142
pub resolved_product_image: ResolvedProductImage,
138143
pub cluster_config: ValidatedClusterConfig,
139144
pub role_config: ValidatedRoleConfig,
@@ -167,41 +172,56 @@ pub struct ValidatedRoleConfig {
167172
}
168173

169174
impl ValidatedSparkConnectServer {
175+
/// Recommended labels for a resource that is not tied to a concrete [`SparkConnectRole`]
176+
/// (e.g. the cluster-shared RBAC resources), using a free-form role/role-group label value.
177+
pub fn recommended_labels_for(
178+
&self,
179+
role_name: &RoleName,
180+
role_group_name: &RoleGroupName,
181+
) -> Labels {
182+
self.recommended_labels_with(&self.product_version, role_name, role_group_name)
183+
}
184+
170185
/// Recommended labels for a resource of the given role.
171-
pub(crate) fn recommended_labels(&self, role: SparkConnectRole) -> Labels {
172-
// `app_version_label_value` is constructed to be a valid label value, so it is also a
173-
// valid `ProductVersion`.
174-
let product_version =
175-
ProductVersion::from_str(&self.resolved_product_image.app_version_label_value)
176-
.expect("the app version label value is a valid product version");
177-
let role_group = RoleGroupName::from_str(DEFAULT_SPARK_CONNECT_GROUP_NAME)
178-
.expect("DEFAULT_SPARK_CONNECT_GROUP_NAME is a valid role group name");
186+
pub fn recommended_labels(&self, role: SparkConnectRole) -> Labels {
187+
self.recommended_labels_for(&role_name(role), &DEFAULT_SPARK_CONNECT_ROLE_GROUP)
188+
}
189+
190+
fn recommended_labels_with(
191+
&self,
192+
product_version: &ProductVersion,
193+
role_name: &RoleName,
194+
role_group_name: &RoleGroupName,
195+
) -> Labels {
179196
recommended_labels(
180197
self,
181198
&product_name(),
182-
&product_version,
199+
product_version,
183200
&operator_name(),
184201
&controller_name(),
185-
&role_name(role),
186-
&role_group,
202+
role_name,
203+
role_group_name,
187204
)
188205
}
189206

190207
/// Selector labels matching the pods of the given role.
191-
pub(crate) fn role_selector(&self, role: SparkConnectRole) -> Labels {
208+
pub fn role_selector(&self, role: SparkConnectRole) -> Labels {
192209
role_selector(self, &product_name(), &role_name(role))
193210
}
194211

195212
/// Selector labels matching the pods of the given role's (single) role group.
196-
pub(crate) fn role_group_selector(&self, role: SparkConnectRole) -> Labels {
197-
let role_group = RoleGroupName::from_str(DEFAULT_SPARK_CONNECT_GROUP_NAME)
198-
.expect("DEFAULT_SPARK_CONNECT_GROUP_NAME is a valid role group name");
199-
role_group_selector(self, &product_name(), &role_name(role), &role_group)
213+
pub fn role_group_selector(&self, role: SparkConnectRole) -> Labels {
214+
role_group_selector(
215+
self,
216+
&product_name(),
217+
&role_name(role),
218+
&DEFAULT_SPARK_CONNECT_ROLE_GROUP,
219+
)
200220
}
201221

202222
/// Object metadata for a child resource named `name`, owned by this SparkConnectServer and
203223
/// carrying the recommended labels for the given role.
204-
pub(crate) fn object_meta(
224+
pub fn object_meta(
205225
&self,
206226
name: impl Into<String>,
207227
role: SparkConnectRole,
@@ -214,20 +234,29 @@ impl ValidatedSparkConnectServer {
214234
.with_labels(self.recommended_labels(role));
215235
builder
216236
}
237+
238+
/// Type-safe names for the per-cluster RBAC resources: the ServiceAccount,
239+
/// its (namespaced) RoleBinding, and the operator-deployed ClusterRole it binds.
240+
pub fn rbac_resource_names(&self) -> role_utils::ResourceNames {
241+
role_utils::ResourceNames {
242+
cluster_name: self.name.clone(),
243+
product_name: product_name(),
244+
}
245+
}
217246
}
218247

219248
/// The product name (`spark-connect`) as a type-safe label value.
220-
pub(crate) fn product_name() -> ProductName {
249+
pub fn product_name() -> ProductName {
221250
ProductName::from_str(CONNECT_APP_NAME).expect("CONNECT_APP_NAME is a valid product name")
222251
}
223252

224253
/// The operator name as a type-safe label value.
225-
pub(crate) fn operator_name() -> OperatorName {
254+
pub fn operator_name() -> OperatorName {
226255
OperatorName::from_str(OPERATOR_NAME).expect("the operator name is a valid label value")
227256
}
228257

229258
/// The controller name as a type-safe label value.
230-
pub(crate) fn controller_name() -> ControllerName {
259+
pub fn controller_name() -> ControllerName {
231260
ControllerName::from_str(CONNECT_CONTROLLER_NAME)
232261
.expect("the controller name is a valid label value")
233262
}
@@ -304,6 +333,9 @@ pub fn validate(
304333
)
305334
.context(ResolveProductImageSnafu)?;
306335

336+
let product_version = ProductVersion::from_str(&resolved_product_image.app_version_label_value)
337+
.expect("the app version label value is a valid product version");
338+
307339
let server_config = scs.server_config().context(ServerConfigSnafu)?;
308340
let executor_config = scs.executor_config().context(ExecutorConfigSnafu)?;
309341

@@ -350,6 +382,7 @@ pub fn validate(
350382
name,
351383
namespace,
352384
uid,
385+
product_version,
353386
resolved_product_image,
354387
cluster_config: ValidatedClusterConfig {
355388
resolved_s3: dereferenced.resolved_s3,

0 commit comments

Comments
 (0)