Skip to content

Commit f05b6b8

Browse files
committed
refactor history server service account and role binding, added tests
1 parent e43638b commit f05b6b8

9 files changed

Lines changed: 317 additions & 107 deletions

File tree

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,6 @@ pub enum Error {
6565
source: error_boundary::InvalidObject,
6666
},
6767

68-
#[snafu(display("failed to build RBAC resources"))]
69-
BuildRbacResources {
70-
source: stackable_operator::commons::rbac::Error,
71-
},
72-
7368
#[snafu(display("failed to dereference SparkConnectServer"))]
7469
DereferenceSparkConnectServer { source: dereference::Error },
7570

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,98 @@ pub(crate) fn build(
109109
stateful_set,
110110
})
111111
}
112+
113+
#[cfg(test)]
114+
mod tests {
115+
use std::collections::BTreeMap;
116+
117+
use indoc::indoc;
118+
use stackable_operator::{cli::OperatorEnvironmentOptions, utils::yaml_from_str_singleton_map};
119+
120+
use super::build;
121+
use crate::connect::{
122+
controller::{
123+
dereference::DereferencedSparkConnectServer,
124+
validate::{ValidatedSparkConnectServer, validate},
125+
},
126+
crd::v1alpha1,
127+
s3::ResolvedS3,
128+
};
129+
130+
/// Minimal S3-free `SparkConnectServer` fixture, keeping the dereference step client-free;
131+
/// the `uid` allows owner references to be derived from it.
132+
const CONNECT_YAML: &str = indoc! {r#"
133+
apiVersion: spark.stackable.tech/v1alpha1
134+
kind: SparkConnectServer
135+
metadata:
136+
name: my-connect
137+
namespace: default
138+
uid: 12345678-1234-1234-1234-123456789012
139+
spec:
140+
image:
141+
productVersion: 4.1.2
142+
"#};
143+
144+
/// Runs the real validate step against the minimal fixture.
145+
fn minimal_validated_cluster() -> ValidatedSparkConnectServer {
146+
let scs: v1alpha1::SparkConnectServer = yaml_from_str_singleton_map(CONNECT_YAML)
147+
.expect("invalid test SparkConnectServer YAML");
148+
validate(
149+
&scs,
150+
DereferencedSparkConnectServer {
151+
resolved_s3: ResolvedS3::none(),
152+
},
153+
&OperatorEnvironmentOptions {
154+
operator_namespace: "stackable-operators".to_string(),
155+
operator_service_name: "spark-k8s-operator".to_string(),
156+
image_repository: "oci.example.org/sdp".to_string(),
157+
},
158+
)
159+
.expect("validate should succeed for the test fixture")
160+
}
161+
162+
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
163+
/// accidental drift. The fixture's cluster name deliberately differs from the product name so
164+
/// that swapped `name`/`instance` label values cannot pass unnoticed.
165+
#[test]
166+
fn build_produces_rbac() {
167+
let resources = build(&minimal_validated_cluster(), &[]).expect("build succeeds");
168+
169+
assert_eq!(
170+
resources.service_account.metadata.name.as_deref(),
171+
Some("my-connect-serviceaccount")
172+
);
173+
assert_eq!(
174+
resources.role_binding.metadata.name.as_deref(),
175+
Some("my-connect-rolebinding")
176+
);
177+
178+
let expected_labels = BTreeMap::from(
179+
[
180+
("app.kubernetes.io/component", "none"),
181+
("app.kubernetes.io/instance", "my-connect"),
182+
(
183+
"app.kubernetes.io/managed-by",
184+
"spark.stackable.tech_connect",
185+
),
186+
("app.kubernetes.io/name", "spark-connect"),
187+
("app.kubernetes.io/role-group", "none"),
188+
("app.kubernetes.io/version", "4.1.2-stackable0.0.0-dev"),
189+
("stackable.tech/vendor", "Stackable"),
190+
]
191+
.map(|(key, value)| (key.to_string(), value.to_string())),
192+
);
193+
assert_eq!(
194+
resources.service_account.metadata.labels,
195+
Some(expected_labels.clone())
196+
);
197+
assert_eq!(
198+
resources.role_binding.metadata.labels,
199+
Some(expected_labels)
200+
);
201+
assert_eq!(
202+
resources.role_binding.role_ref.name,
203+
"spark-connect-clusterrole"
204+
);
205+
}
206+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ pub(crate) struct ResolvedS3 {
5454
}
5555

5656
impl ResolvedS3 {
57+
/// The resolved form of a SparkConnectServer without any S3 connectors, for tests that must
58+
/// stay client-free.
59+
#[cfg(test)]
60+
pub(crate) fn none() -> Self {
61+
Self {
62+
s3_buckets: Vec::new(),
63+
s3_connection: None,
64+
}
65+
}
66+
5767
pub(crate) async fn resolve(
5868
client: &stackable_operator::client::Client,
5969
connect_server: &crd::v1alpha1::SparkConnectServer,

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

Lines changed: 4 additions & 42 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,
@@ -21,10 +20,7 @@ use stackable_operator::{
2120
};
2221
use strum::{EnumDiscriminants, IntoStaticStr};
2322

24-
use crate::{
25-
Ctx,
26-
crd::{constants::HISTORY_APP_NAME, history::v1alpha1},
27-
};
23+
use crate::{Ctx, crd::history::v1alpha1};
2824

2925
pub mod build;
3026
pub mod dereference;
@@ -34,11 +30,6 @@ pub mod validate;
3430
#[strum_discriminants(derive(IntoStaticStr))]
3531
#[allow(clippy::enum_variant_names)]
3632
pub enum Error {
37-
#[snafu(display("failed to build RBAC resources"))]
38-
BuildRbacResources {
39-
source: stackable_operator::commons::rbac::Error,
40-
},
41-
4233
#[snafu(display("failed to build SparkHistoryServer resources"))]
4334
BuildSparkHistoryServer { source: build::Error },
4435

@@ -47,16 +38,6 @@ pub enum Error {
4738
source: stackable_operator::cluster_resources::Error,
4839
},
4940

50-
#[snafu(display("failed to apply role ServiceAccount"))]
51-
ApplyServiceAccount {
52-
source: stackable_operator::cluster_resources::Error,
53-
},
54-
55-
#[snafu(display("failed to apply global RoleBinding"))]
56-
ApplyRoleBinding {
57-
source: stackable_operator::cluster_resources::Error,
58-
},
59-
6041
#[snafu(display("failed to dereference SparkHistoryServer"))]
6142
DereferenceSparkHistoryServer { source: dereference::Error },
6243

@@ -68,12 +49,6 @@ pub enum Error {
6849
source: stackable_operator::cluster_resources::Error,
6950
},
7051

71-
#[snafu(display("failed to get required Labels"))]
72-
GetRequiredLabels {
73-
source:
74-
stackable_operator::kvp::KeyValuePairError<stackable_operator::kvp::LabelValueError>,
75-
},
76-
7752
#[snafu(display("SparkHistoryServer object is invalid"))]
7853
InvalidSparkHistoryServer {
7954
// boxed because otherwise Clippy warns about a large enum variant
@@ -135,32 +110,19 @@ pub async fn reconcile(
135110
&shs.spec.object_overrides,
136111
);
137112

138-
// Use a dedicated service account for history server pods. Building the RBAC resources needs
139-
// the cluster-resource labels, so it stays in the reconcile step; the built objects (whose
140-
// names are deterministic) are handed to the client-free build step.
141-
let (service_account, role_binding) = build_rbac_resources(
142-
shs,
143-
HISTORY_APP_NAME,
144-
cluster_resources
145-
.get_required_labels()
146-
.context(GetRequiredLabelsSnafu)?,
147-
)
148-
.context(BuildRbacResourcesSnafu)?;
149-
150-
let resources = build::build(&validated, service_account, role_binding)
151-
.context(BuildSparkHistoryServerSnafu)?;
113+
let resources = build::build(&validated).context(BuildSparkHistoryServerSnafu)?;
152114

153115
// Apply order: ServiceAccount and RoleBinding first, then the ConfigMaps, metrics Services,
154116
// Listener and PodDisruptionBudget, and finally the StatefulSets (they mount the ConfigMaps
155117
// and run under the SA, so those must exist first).
156118
cluster_resources
157119
.add(client, resources.service_account)
158120
.await
159-
.context(ApplyServiceAccountSnafu)?;
121+
.context(ApplyResourceSnafu)?;
160122
cluster_resources
161123
.add(client, resources.role_binding)
162124
.await
163-
.context(ApplyRoleBindingSnafu)?;
125+
.context(ApplyResourceSnafu)?;
164126
for config_map in resources.config_maps {
165127
cluster_resources
166128
.add(client, config_map)

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

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pub mod resource;
22

33
use snafu::{ResultExt, Snafu};
4-
use stackable_operator::k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding};
54

65
use crate::{
76
crd::constants::HISTORY_ROLE_NAME,
@@ -11,6 +10,7 @@ use crate::{
1110
config_map::{self, build_config_map},
1211
listener::build_group_listener,
1312
pdb::build_pdb,
13+
rbac::{build_role_binding, build_service_account},
1414
service::build_rolegroup_metrics_service,
1515
statefulset::{self, build_stateful_set},
1616
},
@@ -30,11 +30,7 @@ pub enum Error {
3030
type Result<T, E = Error> = std::result::Result<T, E>;
3131

3232
/// Builds every Kubernetes resource for the given validated SparkHistoryServer.
33-
pub fn build(
34-
validated: &ValidatedSparkHistoryServer,
35-
service_account: ServiceAccount,
36-
role_binding: RoleBinding,
37-
) -> Result<SparkHistoryResources> {
33+
pub fn build(validated: &ValidatedSparkHistoryServer) -> Result<SparkHistoryResources> {
3834
let log_dir = &validated.cluster_config.log_dir;
3935

4036
let mut config_maps = vec![];
@@ -46,7 +42,7 @@ pub fn build(
4642
.push(build_config_map(validated, role_group_name, rg).context(BuildConfigMapSnafu)?);
4743
metrics_services.push(build_rolegroup_metrics_service(validated, role_group_name));
4844
stateful_sets.push(
49-
build_stateful_set(validated, role_group_name, rg, log_dir, &service_account)
45+
build_stateful_set(validated, role_group_name, rg, log_dir)
5046
.context(BuildStatefulSetSnafu)?,
5147
);
5248
}
@@ -60,12 +56,112 @@ pub fn build(
6056
let pod_disruption_budget = build_pdb(&validated.role_config.pdb, validated);
6157

6258
Ok(SparkHistoryResources {
63-
service_account,
64-
role_binding,
59+
service_account: build_service_account(validated),
60+
role_binding: build_role_binding(validated),
6561
config_maps,
6662
metrics_services,
6763
stateful_sets,
6864
listener,
6965
pod_disruption_budget,
7066
})
7167
}
68+
69+
#[cfg(test)]
70+
mod tests {
71+
use std::collections::BTreeMap;
72+
73+
use indoc::indoc;
74+
use stackable_operator::{cli::OperatorEnvironmentOptions, utils::yaml_from_str_singleton_map};
75+
76+
use super::build;
77+
use crate::{
78+
crd::{history::v1alpha1, logdir::ResolvedLogDir},
79+
history::controller::{
80+
dereference::DereferencedSparkHistoryServer,
81+
validate::{ValidatedSparkHistoryServer, validate},
82+
},
83+
};
84+
85+
/// Minimal custom-log-dir `SparkHistoryServer` fixture. The custom log directory keeps the
86+
/// dereference step client-free; the `uid` allows owner references to be derived from it.
87+
const HISTORY_YAML: &str = indoc! {r#"
88+
apiVersion: spark.stackable.tech/v1alpha1
89+
kind: SparkHistoryServer
90+
metadata:
91+
name: my-history
92+
namespace: default
93+
uid: 12345678-1234-1234-1234-123456789012
94+
spec:
95+
image:
96+
productVersion: 3.5.8
97+
logFileDirectory:
98+
customLogDirectory: file:///stackable/spark/logs
99+
nodes:
100+
roleGroups:
101+
default:
102+
replicas: 1
103+
"#};
104+
105+
/// Runs the real validate step against the minimal fixture.
106+
fn minimal_validated_cluster() -> ValidatedSparkHistoryServer {
107+
let shs: v1alpha1::SparkHistoryServer = yaml_from_str_singleton_map(HISTORY_YAML)
108+
.expect("invalid test SparkHistoryServer YAML");
109+
validate(
110+
&shs,
111+
DereferencedSparkHistoryServer {
112+
log_dir: ResolvedLogDir::Custom("file:///stackable/spark/logs".to_string()),
113+
},
114+
&OperatorEnvironmentOptions {
115+
operator_namespace: "stackable-operators".to_string(),
116+
operator_service_name: "spark-k8s-operator".to_string(),
117+
image_repository: "oci.example.org/sdp".to_string(),
118+
},
119+
)
120+
.expect("validate should succeed for the test fixture")
121+
}
122+
123+
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
124+
/// accidental drift. The fixture's cluster name deliberately differs from the product name so
125+
/// that swapped `name`/`instance` label values cannot pass unnoticed.
126+
#[test]
127+
fn build_produces_rbac() {
128+
let resources = build(&minimal_validated_cluster()).expect("build succeeds");
129+
130+
assert_eq!(
131+
resources.service_account.metadata.name.as_deref(),
132+
Some("my-history-serviceaccount")
133+
);
134+
assert_eq!(
135+
resources.role_binding.metadata.name.as_deref(),
136+
Some("my-history-rolebinding")
137+
);
138+
139+
let expected_labels = BTreeMap::from(
140+
[
141+
("app.kubernetes.io/component", "none"),
142+
("app.kubernetes.io/instance", "my-history"),
143+
(
144+
"app.kubernetes.io/managed-by",
145+
"spark.stackable.tech_history",
146+
),
147+
("app.kubernetes.io/name", "spark-history"),
148+
("app.kubernetes.io/role-group", "none"),
149+
("app.kubernetes.io/version", "3.5.8-stackable0.0.0-dev"),
150+
("stackable.tech/vendor", "Stackable"),
151+
]
152+
.map(|(key, value)| (key.to_string(), value.to_string())),
153+
);
154+
assert_eq!(
155+
resources.service_account.metadata.labels,
156+
Some(expected_labels.clone())
157+
);
158+
assert_eq!(
159+
resources.role_binding.metadata.labels,
160+
Some(expected_labels)
161+
);
162+
assert_eq!(
163+
resources.role_binding.role_ref.name,
164+
"spark-history-clusterrole"
165+
);
166+
}
167+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod config_map;
22
pub mod listener;
33
pub mod pdb;
4+
pub mod rbac;
45
pub mod service;
56
pub mod statefulset;

0 commit comments

Comments
 (0)