11use stackable_operator:: crd:: s3;
22
33use crate :: crd:: {
4+ Error ,
45 constants:: {
56 JVM_SECURITY_PROPERTIES_FILE , OPENLINEAGE_ADD_OPENS , STACKABLE_TLS_STORE_PASSWORD ,
67 STACKABLE_TRUST_STORE , VOLUME_MOUNT_PATH_LOG_CONFIG ,
78 } ,
89 logdir:: ResolvedLogDir ,
10+ spark_major_version,
911 tlscerts:: tls_secret_names,
1012 v1alpha1:: SparkApplication ,
1113} ;
@@ -16,11 +18,15 @@ use crate::crd::{
1618///
1719/// Returns `(driver, executor)`: the operator-generated base arguments with the role's
1820/// `jvmArgumentOverrides` applied on top.
21+ ///
22+ /// `product_version` is the resolved Spark product version (e.g. `4.1.2`); it gates the
23+ /// version-specific OpenLineage `--add-opens` flag.
1924pub fn construct_extra_java_options (
2025 spark_application : & SparkApplication ,
2126 s3_conn : & Option < s3:: v1alpha1:: ConnectionSpec > ,
2227 log_dir : & Option < ResolvedLogDir > ,
23- ) -> ( String , String ) {
28+ product_version : & str ,
29+ ) -> Result < ( String , String ) , Error > {
2430 // Note (@sbernauer): As of 2025-03-04, we did not set any heap related JVM arguments, so I
2531 // kept the implementation as is. We can always re-visit this as needed.
2632
@@ -39,7 +45,10 @@ pub fn construct_extra_java_options(
3945 // OpenLineage on Spark 4.x needs `java.base/java.security` opened to the unnamed module,
4046 // otherwise the driver throws a non-fatal `InaccessibleObjectException` and silently degrades
4147 // extension-interface lineage (MVP §7). Added to both driver and executor.
42- if spark_application. spec . open_lineage . is_some ( ) {
48+ //
49+ // This is scoped to Spark 4.x: the flag is unnecessary — and on the JDK 17 Spark 3.5.x images the
50+ // operator also ships, potentially a startup error — so it must not be emitted there.
51+ if spark_application. openlineage_enabled ( ) && spark_major_version ( product_version) ? >= 4 {
4352 jvm_args. push ( OPENLINEAGE_ADD_OPENS . to_string ( ) ) ;
4453 }
4554
@@ -63,16 +72,24 @@ pub fn construct_extra_java_options(
6372 None => jvm_args. clone ( ) ,
6473 } ;
6574
66- ( driver. join ( " " ) , executor. join ( " " ) )
75+ Ok ( ( driver. join ( " " ) , executor. join ( " " ) ) )
6776}
6877
6978#[ cfg( test) ]
7079mod tests {
80+ use rstest:: rstest;
81+
7182 use super :: * ;
7283
84+ fn spark_app_from_yaml ( input : & str ) -> SparkApplication {
85+ let deserializer = serde_yaml:: Deserializer :: from_str ( input) ;
86+ serde_yaml:: with:: singleton_map_recursive:: deserialize ( deserializer) . unwrap ( )
87+ }
88+
7389 #[ test]
7490 fn test_construct_jvm_arguments_defaults ( ) {
75- let input = r#"
91+ let spark_app = spark_app_from_yaml (
92+ r#"
7693 apiVersion: spark.stackable.tech/v1alpha1
7794 kind: SparkApplication
7895 metadata:
@@ -81,14 +98,11 @@ mod tests {
8198 mode: cluster
8299 mainApplicationFile: test.py
83100 sparkImage:
84- productVersion: 1.2.3
85- "# ;
86-
87- let deserializer = serde_yaml:: Deserializer :: from_str ( input) ;
88- let spark_app: SparkApplication =
89- serde_yaml:: with:: singleton_map_recursive:: deserialize ( deserializer) . unwrap ( ) ;
101+ productVersion: 4.1.2
102+ "# ,
103+ ) ;
90104 let ( driver_extra_java_options, executor_extra_java_options) =
91- construct_extra_java_options ( & spark_app, & None , & None ) ;
105+ construct_extra_java_options ( & spark_app, & None , & None , "4.1.2" ) . unwrap ( ) ;
92106
93107 assert_eq ! (
94108 driver_extra_java_options,
@@ -102,7 +116,8 @@ mod tests {
102116
103117 #[ test]
104118 fn test_construct_jvm_argument_overrides ( ) {
105- let input = r#"
119+ let spark_app = spark_app_from_yaml (
120+ r#"
106121 apiVersion: spark.stackable.tech/v1alpha1
107122 kind: SparkApplication
108123 metadata:
@@ -111,7 +126,7 @@ mod tests {
111126 mode: cluster
112127 mainApplicationFile: test.py
113128 sparkImage:
114- productVersion: 1.2.3
129+ productVersion: 4. 1.2
115130 driver:
116131 jvmArgumentOverrides:
117132 add:
@@ -122,13 +137,10 @@ mod tests {
122137 - -Dhttps.proxyHost=from-executor
123138 removeRegex:
124139 - -Djava.security.properties=.*
125- "# ;
126-
127- let deserializer = serde_yaml:: Deserializer :: from_str ( input) ;
128- let spark_app: SparkApplication =
129- serde_yaml:: with:: singleton_map_recursive:: deserialize ( deserializer) . unwrap ( ) ;
140+ "# ,
141+ ) ;
130142 let ( driver_extra_java_options, executor_extra_java_options) =
131- construct_extra_java_options ( & spark_app, & None , & None ) ;
143+ construct_extra_java_options ( & spark_app, & None , & None , "4.1.2" ) . unwrap ( ) ;
132144
133145 assert_eq ! (
134146 driver_extra_java_options,
@@ -140,32 +152,59 @@ mod tests {
140152 ) ;
141153 }
142154
143- #[ test]
144- fn test_construct_jvm_arguments_openlineage_add_opens ( ) {
145- let input = r#"
146- apiVersion: spark.stackable.tech/v1alpha1
147- kind: SparkApplication
148- metadata:
149- name: spark-example
150- spec:
151- mode: cluster
152- mainApplicationFile: test.py
153- sparkImage:
154- productVersion: 1.2.3
155- openLineage:
156- connection:
157- inline:
158- host: marquez
159- port: 5000
160- "# ;
161-
162- let deserializer = serde_yaml:: Deserializer :: from_str ( input) ;
163- let spark_app: SparkApplication =
164- serde_yaml:: with:: singleton_map_recursive:: deserialize ( deserializer) . unwrap ( ) ;
155+ const OPENLINEAGE_ENABLED : & str = r#"
156+ apiVersion: spark.stackable.tech/v1alpha1
157+ kind: SparkApplication
158+ metadata:
159+ name: spark-example
160+ spec:
161+ mode: cluster
162+ mainApplicationFile: test.py
163+ sparkImage:
164+ productVersion: PLACEHOLDER
165+ openLineage:
166+ connection:
167+ inline:
168+ host: marquez
169+ port: 5000
170+ "# ;
171+
172+ const OPENLINEAGE_ABSENT : & str = r#"
173+ apiVersion: spark.stackable.tech/v1alpha1
174+ kind: SparkApplication
175+ metadata:
176+ name: spark-example
177+ spec:
178+ mode: cluster
179+ mainApplicationFile: test.py
180+ sparkImage:
181+ productVersion: PLACEHOLDER
182+ "# ;
183+
184+ /// `--add-opens` is emitted only on Spark 4.x with OpenLineage enabled — never on the Scala
185+ /// 2.12 / JDK 17 Spark 3.5.x images, and never when OpenLineage is absent.
186+ #[ rstest]
187+ #[ case:: enabled_spark_3( OPENLINEAGE_ENABLED , "3.5.8" , false ) ]
188+ #[ case:: enabled_spark_4( OPENLINEAGE_ENABLED , "4.1.2" , true ) ]
189+ #[ case:: absent_spark_4( OPENLINEAGE_ABSENT , "4.1.2" , false ) ]
190+ fn test_openlineage_add_opens_is_version_gated (
191+ #[ case] yaml_template : & str ,
192+ #[ case] product_version : & str ,
193+ #[ case] expect_add_opens : bool ,
194+ ) {
195+ let spark_app = spark_app_from_yaml ( & yaml_template. replace ( "PLACEHOLDER" , product_version) ) ;
165196 let ( driver_extra_java_options, executor_extra_java_options) =
166- construct_extra_java_options ( & spark_app, & None , & None ) ;
197+ construct_extra_java_options ( & spark_app, & None , & None , product_version ) . unwrap ( ) ;
167198
168- assert ! ( driver_extra_java_options. contains( OPENLINEAGE_ADD_OPENS ) ) ;
169- assert ! ( executor_extra_java_options. contains( OPENLINEAGE_ADD_OPENS ) ) ;
199+ assert_eq ! (
200+ driver_extra_java_options. contains( OPENLINEAGE_ADD_OPENS ) ,
201+ expect_add_opens,
202+ "driver --add-opens presence mismatch for Spark {product_version}"
203+ ) ;
204+ assert_eq ! (
205+ executor_extra_java_options. contains( OPENLINEAGE_ADD_OPENS ) ,
206+ expect_add_opens,
207+ "executor --add-opens presence mismatch for Spark {product_version}"
208+ ) ;
170209 }
171210}
0 commit comments