From fd3967e176a6854598110e1833c16bedb99df934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCller?= Date: Wed, 2 Sep 2026 19:49:02 +0200 Subject: [PATCH] fix: use sparkImage.pullPolicy in all containers --- CHANGELOG.md | 7 ++ .../pages/usage-guide/job-dependencies.adoc | 6 ++ .../src/connect/controller/build/executor.rs | 46 +++++++++- .../src/connect/controller/build/mod.rs | 26 ++++++ .../src/connect/controller/build/server.rs | 47 +++++++++- rust/operator-binary/src/connect/s3.rs | 64 ++++++++++--- rust/operator-binary/src/crd/mod.rs | 72 ++++++++++++++- .../src/spark_k8s_controller/build/mod.rs | 2 +- .../src/spark_k8s_controller/build/pod.rs | 92 ++++++++++++++++++- .../pyspark-pi-job-template-spec.json | 2 +- .../fixtures/spark-connect-server-data.json | 2 +- 11 files changed, 339 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8770f6..2305c120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,12 @@ All notable changes to this project will be documented in this file. - The history and connect controllers now watch all resources that they create, the missing RBAC `watch` permissions were added, and all controllers early-exit the reconcile action when the object is marked for deletion ([#757]). +- BREAKING (behaviour): `sparkImage.pullPolicy` is no longer ignored by the driver and executor + containers, the Spark Connect executors and the truststore and `job` init containers, and it now + covers the user-supplied `spec.image` too. With the default `Always`, these containers pull on + every start instead of using the node's image cache. The operator also emits the policy as + `spark.kubernetes.container.image.pullPolicy`, which Spark previously left at its default when + rebuilding the containers from the pod templates ([#764]). [#721]: https://github.com/stackabletech/spark-k8s-operator/pull/721 [#727]: https://github.com/stackabletech/spark-k8s-operator/pull/727 @@ -62,6 +68,7 @@ All notable changes to this project will be documented in this file. [#753]: https://github.com/stackabletech/spark-k8s-operator/pull/753 [#754]: https://github.com/stackabletech/spark-k8s-operator/pull/754 [#757]: https://github.com/stackabletech/spark-k8s-operator/pull/757 +[#764]: https://github.com/stackabletech/spark-k8s-operator/pull/764 ## [26.7.0] - 2026-07-21 diff --git a/docs/modules/spark-k8s/pages/usage-guide/job-dependencies.adoc b/docs/modules/spark-k8s/pages/usage-guide/job-dependencies.adoc index d05f33d3..195a447b 100644 --- a/docs/modules/spark-k8s/pages/usage-guide/job-dependencies.adoc +++ b/docs/modules/spark-k8s/pages/usage-guide/job-dependencies.adoc @@ -63,6 +63,12 @@ spec: <1> Reference to your custom image.. <2> Apache Spark version bundled in your custom image. +`sparkImage.pullPolicy` governs every container that the operator selects an image for: the submit, driver and executor containers, the `job`, `requirements` and `tls` init containers, and the Spark Connect server and its executors. +This includes the user-supplied `spec.image`, which the `job` init container runs. + +NOTE: Under a mutable image tag, `IfNotPresent` lets a node serve a stale cached image, so `Always` is the minimum for driver and executors to match the submit pod. +It is not a guarantee, because each container resolves the tag when it starts: pin a digest or a unique tag in `sparkImage.custom` for that. + === Dependency volumes With this method, the job dependencies are provisioned from a `PersistentVolume` as shown in this example: diff --git a/rust/operator-binary/src/connect/controller/build/executor.rs b/rust/operator-binary/src/connect/controller/build/executor.rs index ffbd29dd..367ed0f2 100644 --- a/rust/operator-binary/src/connect/controller/build/executor.rs +++ b/rust/operator-binary/src/connect/controller/build/executor.rs @@ -148,7 +148,7 @@ pub fn executor_pod_template( // S3: Add truststore init container for S3 endpoint communication with TLS. if let Some(truststore_init_container) = resolved_s3 - .truststore_init_container(resolved_product_image.clone()) + .truststore_init_container(resolved_product_image) .context(TrustStoreInitContainerSnafu)? { template.add_init_container(truststore_init_container); @@ -230,6 +230,10 @@ pub(crate) fn executor_properties( "spark.kubernetes.executor.container.image".to_string(), Some(spark_image), ), + ( + "spark.kubernetes.container.image.pullPolicy".to_string(), + Some(resolved_product_image.image_pull_policy.clone()), + ), ( "spark.executor.defaultJavaOptions".to_string(), Some(executor_jvm_args( @@ -371,7 +375,45 @@ mod tests { }; use super::*; - use crate::connect::controller::build::test_support::minimal_validated_cluster; + use crate::connect::controller::build::test_support::{ + PULL_POLICY_NEVER, minimal_validated_cluster, validated_cluster_with_s3_tls, + }; + + #[test] + fn image_pull_policy_is_set_on_every_container_spark_does_not_rebuild() { + let validated = validated_cluster_with_s3_tls(); + let config_map = ConfigMap { + metadata: ObjectMeta { + name: Some("my-connect-executor".to_string()), + ..ObjectMeta::default() + }, + ..ConfigMap::default() + }; + + let pod_spec = executor_pod_template(&validated, &config_map) + .expect("the executor pod template can be built") + .spec + .expect("the executor pod template has a spec"); + + let policies: Vec<(&str, Option<&str>)> = pod_spec + .init_containers + .iter() + .flatten() + .chain(pod_spec.containers.iter()) + .filter(|container| container.name != SparkConnectContainer::Spark.to_string()) + .map(|container| { + ( + container.name.as_str(), + container.image_pull_policy.as_deref(), + ) + }) + .collect(); + + assert_eq!( + vec![("tls-truststore-init", Some(PULL_POLICY_NEVER))], + policies + ); + } /// `envOverrides` must be applied after all operator-set environment variables, so a user /// override replaces the operator-set value instead of duplicating it or being ignored. diff --git a/rust/operator-binary/src/connect/controller/build/mod.rs b/rust/operator-binary/src/connect/controller/build/mod.rs index 2a43fc9a..3c60f1a3 100644 --- a/rust/operator-binary/src/connect/controller/build/mod.rs +++ b/rust/operator-binary/src/connect/controller/build/mod.rs @@ -222,6 +222,14 @@ pub(crate) mod test_support { productVersion: 4.1.2 "#}; + pub const PULL_POLICY_NEVER: &str = "Never"; + + /// [`CONNECT_YAML`] with an explicit `pullPolicy`, appended to the `spec.image` block that + /// [`CONNECT_YAML`] ends with. + fn connect_yaml_with_pull_policy(pull_policy: &str) -> String { + format!("{CONNECT_YAML} pullPolicy: {pull_policy}\n") + } + /// Runs the real validate step against the minimal fixture. pub fn minimal_validated_cluster() -> ValidatedSparkConnectServer { let scs: v1alpha1::SparkConnectServer = yaml_from_str_singleton_map(CONNECT_YAML) @@ -239,4 +247,22 @@ pub(crate) mod test_support { ) .expect("validate should succeed for the test fixture") } + + pub fn validated_cluster_with_s3_tls() -> ValidatedSparkConnectServer { + let scs: v1alpha1::SparkConnectServer = + yaml_from_str_singleton_map(&connect_yaml_with_pull_policy(PULL_POLICY_NEVER)) + .expect("invalid test SparkConnectServer YAML"); + validate( + &scs, + DereferencedSparkConnectServer { + resolved_s3: ResolvedS3::tls_connection(), + }, + &OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_string(), + operator_service_name: "spark-k8s-operator".to_string(), + image_repository: "oci.example.org/sdp".to_string(), + }, + ) + .expect("validate should succeed for the test fixture") + } } diff --git a/rust/operator-binary/src/connect/controller/build/server.rs b/rust/operator-binary/src/connect/controller/build/server.rs index ae074a6b..c1183558 100644 --- a/rust/operator-binary/src/connect/controller/build/server.rs +++ b/rust/operator-binary/src/connect/controller/build/server.rs @@ -317,7 +317,7 @@ pub(crate) fn build_stateful_set( // S3: Add truststore init container for S3 endpoint communication with TLS. if let Some(truststore_init_container) = resolved_s3 - .truststore_init_container(resolved_product_image.clone()) + .truststore_init_container(resolved_product_image) .context(TrustStoreInitContainerSnafu)? { pb.add_init_container(truststore_init_container); @@ -532,7 +532,50 @@ mod tests { }; use super::*; - use crate::connect::controller::build::test_support::minimal_validated_cluster; + use crate::connect::controller::build::test_support::{ + PULL_POLICY_NEVER, minimal_validated_cluster, validated_cluster_with_s3_tls, + }; + + #[test] + fn image_pull_policy_is_set_on_every_server_container() { + let validated = validated_cluster_with_s3_tls(); + let config_map = ConfigMap { + metadata: ObjectMeta { + name: Some("my-connect-server".to_string()), + ..ObjectMeta::default() + }, + ..ConfigMap::default() + }; + + let pod_spec = build_stateful_set(&validated, &config_map, "my-connect-server", vec![]) + .expect("the StatefulSet can be built") + .spec + .expect("the StatefulSet has a spec") + .template + .spec + .expect("the StatefulSet has a pod spec"); + + let policies: Vec<(&str, Option<&str>)> = pod_spec + .init_containers + .iter() + .flatten() + .chain(pod_spec.containers.iter()) + .map(|container| { + ( + container.name.as_str(), + container.image_pull_policy.as_deref(), + ) + }) + .collect(); + + assert_eq!( + vec![ + ("tls-truststore-init", Some(PULL_POLICY_NEVER)), + ("spark", Some(PULL_POLICY_NEVER)), + ], + policies + ); + } /// `envOverrides` must be applied after all operator-set environment variables, so a user /// override replaces the operator-set value instead of duplicating it or being ignored. diff --git a/rust/operator-binary/src/connect/s3.rs b/rust/operator-binary/src/connect/s3.rs index 0e46f89f..bad11b8f 100644 --- a/rust/operator-binary/src/connect/s3.rs +++ b/rust/operator-binary/src/connect/s3.rs @@ -1,10 +1,14 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::{ + collections::{BTreeMap, BTreeSet}, + str::FromStr, +}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::product_image_selection::ResolvedProductImage, crd::s3::{self, v1alpha1::S3AccessStyle}, - k8s_openapi::api::core::v1::{Volume, VolumeMount}, + k8s_openapi::api::core::v1::{Container, Volume, VolumeMount}, + v2::{builder::pod::container::new_container_builder, types::kubernetes::ContainerName}, }; use crate::{ @@ -17,6 +21,8 @@ use crate::{ }, }; +const TRUSTSTORE_INIT_CONTAINER_NAME: &str = "tls-truststore-init"; + #[derive(Snafu, Debug)] #[allow(clippy::enum_variant_names)] pub enum Error { @@ -42,6 +48,11 @@ pub enum Error { source: s3::v1alpha1::ConnectionError, }, + #[snafu(display("failed to add a volume mount to the truststore init container"))] + AddVolumeMount { + source: stackable_operator::builder::pod::container::Error, + }, + #[snafu(display("failed to get volumes and mounts for S3 connection"))] ConnectionVolumesAndMounts { source: s3::v1alpha1::ConnectionError, @@ -64,6 +75,33 @@ impl ResolvedS3 { } } + #[cfg(test)] + pub(crate) fn tls_connection() -> Self { + use stackable_operator::commons::tls_verification::{ + CaCert, Tls, TlsClientDetails, TlsServerVerification, TlsVerification, + }; + + Self { + s3_buckets: Vec::new(), + s3_connection: Some(s3::v1alpha1::ConnectionSpec { + host: "my-s3-endpoint.com".parse().expect("a valid host"), + port: None, + region: s3::v1alpha1::Region { + name: "us-east-1".to_string(), + }, + access_style: S3AccessStyle::Path, + credentials: None, + tls: TlsClientDetails { + tls: Some(Tls { + verification: TlsVerification::Server(TlsServerVerification { + ca_cert: CaCert::SecretClass("tls-ca-secret-class".to_string()), + }), + }), + }, + }), + } + } + pub(crate) async fn resolve( client: &stackable_operator::client::Client, connect_server: &crd::v1alpha1::SparkConnectServer, @@ -269,25 +307,27 @@ impl ResolvedS3 { pub(crate) fn truststore_init_container( &self, - image: ResolvedProductImage, - ) -> Result, Error> { + image: &ResolvedProductImage, + ) -> Result, Error> { if let Some(command) = self.truststore_init_container_command() { let (_, volume_mounts) = self.volumes_and_mounts()?; + let name = ContainerName::from_str(TRUSTSTORE_INIT_CONTAINER_NAME) + .expect("TRUSTSTORE_INIT_CONTAINER_NAME is a valid container name"); + Ok(Some( - stackable_operator::k8s_openapi::api::core::v1::Container { - name: "tls-truststore-init".to_string(), - image: Some(image.image), - command: Some(vec![ + new_container_builder(&name) + .image_from_product_image(image) + .command(vec![ "/bin/bash".to_string(), "-x".to_string(), "-euo".to_string(), "pipefail".to_string(), "-c".to_string(), command, - ]), - volume_mounts: Some(volume_mounts), - ..Default::default() - }, + ]) + .add_volume_mounts(volume_mounts) + .context(AddVolumeMountSnafu)? + .build(), )) } else { Ok(None) diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 1b9b824e..373bdbae 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -17,7 +17,7 @@ use stackable_operator::{ VolumeBuilder, }, commons::{ - product_image_selection::ProductImage, + product_image_selection::{ProductImage, ResolvedProductImage}, resources::{CpuLimits, MemoryLimits, Resources}, secret_class::SecretClassVolumeProvisionParts, }, @@ -581,7 +581,7 @@ impl v1alpha1::SparkApplication { &self, s3conn: &Option, log_dir: &Option, - spark_image: &str, + spark_image: &ResolvedProductImage, ) -> Result, Error> { // mandatory properties let mode = &self.spec.mode; @@ -631,11 +631,11 @@ impl v1alpha1::SparkApplication { ), format!( "--conf spark.kubernetes.driver.container.image={}", - spark_image.to_string() + spark_image.image ), format!( "--conf spark.kubernetes.executor.container.image={}", - spark_image.to_string() + spark_image.image ), format!( "--conf spark.driver.defaultJavaOptions=-Dlog4j.configurationFile={VOLUME_MOUNT_PATH_LOG_CONFIG}/{LOG4J2_CONFIG_FILE}" @@ -723,6 +723,11 @@ impl v1alpha1::SparkApplication { "0.0".to_string(), ); + submit_conf.insert( + "spark.kubernetes.container.image.pullPolicy".to_string(), + spark_image.image_pull_policy.clone(), + ); + resources_to_driver_props( self.spec.main_class.is_some(), &self.driver_config()?, @@ -1599,6 +1604,65 @@ spec: assert_eq!(got, expected); } + #[rstest] + #[case::from_spark_image(None, "IfNotPresent")] + #[case::spark_conf_wins(Some("Never"), "Never")] + fn spark_image_pull_policy_is_passed_to_spark_submit( + #[case] spark_conf_override: Option<&str>, + #[case] expected: &str, + ) { + let spark_conf = spark_conf_override + .map(|policy| { + format!("\n sparkConf:\n spark.kubernetes.container.image.pullPolicy: {policy}") + }) + .unwrap_or_default(); + let yaml = format!( + indoc! {r#" + apiVersion: spark.stackable.tech/v1alpha1 + kind: SparkApplication + metadata: + name: spark-example + namespace: default + spec: + mode: cluster + mainApplicationFile: test.py + sparkImage: + productVersion: 1.2.3 + pullPolicy: IfNotPresent{spark_conf} + "#}, + spark_conf = spark_conf + ); + let deserializer = serde_yaml::Deserializer::from_str(&yaml); + let spark_application: v1alpha1::SparkApplication = + serde_yaml::with::singleton_map_recursive::deserialize(deserializer) + .expect("invalid test SparkApplication YAML"); + + let resolved_product_image = spark_application + .spec + .spark_image + .resolve("spark-k8s", "oci.example.org/sdp", "0.0.0-dev") + .expect("the product image resolves"); + + let command = spark_application + .build_command(&None, &None, &resolved_product_image) + .expect("the submit command can be built") + .join(" "); + + let occurrences = command + .matches("spark.kubernetes.container.image.pullPolicy=") + .count(); + assert_eq!( + 1, occurrences, + "the property must be passed exactly once, so that it is unambiguous" + ); + assert!( + command.contains(&format!( + r#"--conf "spark.kubernetes.container.image.pullPolicy={expected}""# + )), + "expected pull policy {expected} in: {command}" + ); + } + impl RoundtripTestData for v1alpha1::SparkApplicationSpec { fn roundtrip_test_data() -> Vec { stackable_operator::utils::yaml_from_str_singleton_map(indoc! {r#" diff --git a/rust/operator-binary/src/spark_k8s_controller/build/mod.rs b/rust/operator-binary/src/spark_k8s_controller/build/mod.rs index 68ceb2dc..cae98917 100644 --- a/rust/operator-binary/src/spark_k8s_controller/build/mod.rs +++ b/rust/operator-binary/src/spark_k8s_controller/build/mod.rs @@ -109,7 +109,7 @@ pub fn build(validated: &ValidatedSparkApplication) -> Result)> = init_containers(&validated, &logging) + .expect("the init containers can be built") + .into_iter() + .map(|container| (container.name, container.image_pull_policy)) + .collect(); + + assert_eq!( + vec![ + ("job".to_string(), Some("Always".to_string())), + ("requirements".to_string(), Some("Always".to_string())), + ("tls".to_string(), Some("Always".to_string())), + ], + policies + ); + } + /// `envOverrides` must be applied after all operator-set environment variables, so a user /// override replaces the operator-set value instead of duplicating it or being ignored. #[test] diff --git a/tests/templates/kuttl/product-config-compat/fixtures/pyspark-pi-job-template-spec.json b/tests/templates/kuttl/product-config-compat/fixtures/pyspark-pi-job-template-spec.json index 976109ef..04495329 100644 --- a/tests/templates/kuttl/product-config-compat/fixtures/pyspark-pi-job-template-spec.json +++ b/tests/templates/kuttl/product-config-compat/fixtures/pyspark-pi-job-template-spec.json @@ -3,7 +3,7 @@ "containers": [ { "args": [ - "containerdebug --output=/stackable/log/containerdebug-state.json --loop & /stackable/spark/bin/spark-submit --verbose --master k8s://https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS} --deploy-mode cluster --name pyspark-pi --conf spark.kubernetes.driver.podTemplateFile=/stackable/spark/driver-pod-templates/template.yaml --conf spark.kubernetes.executor.podTemplateFile=/stackable/spark/executor-pod-templates/template.yaml --conf spark.kubernetes.driver.podTemplateContainerName=spark --conf spark.kubernetes.executor.podTemplateContainerName=spark --conf spark.kubernetes.namespace=__NAMESPACE__ --conf spark.kubernetes.driver.container.image=oci.stackable.tech/sdp/spark-k8s:3.5.8-stackable0.0.0-dev --conf spark.kubernetes.executor.container.image=oci.stackable.tech/sdp/spark-k8s:3.5.8-stackable0.0.0-dev --conf spark.driver.defaultJavaOptions=-Dlog4j.configurationFile=/stackable/log_config/log4j2.properties --conf spark.driver.extraClassPath=/stackable/spark/extra-jars/* --conf spark.executor.defaultJavaOptions=-Dlog4j.configurationFile=/stackable/log_config/log4j2.properties --conf spark.executor.extraClassPath=/stackable/spark/extra-jars/* --conf spark.driver.extraJavaOptions=\"-Djava.security.properties=/stackable/log_config/security.properties\" --conf spark.executor.extraJavaOptions=\"-Djava.security.properties=/stackable/log_config/security.properties\" --conf spark.metrics.conf.\\*.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet --conf spark.metrics.conf.\\*.sink.prometheusServlet.path=/metrics/prometheus --conf spark.ui.prometheus.enabled=true --conf spark.sql.streaming.metricsEnabled=true --conf \"spark.driver.cores=2\" --conf \"spark.driver.memory=640m\" --conf \"spark.executor.cores=2\" --conf \"spark.executor.instances=1\" --conf \"spark.executor.memory=640m\" --conf \"spark.kubernetes.driver.limit.cores=2\" --conf \"spark.kubernetes.driver.request.cores=1\" --conf \"spark.kubernetes.executor.limit.cores=2\" --conf \"spark.kubernetes.executor.request.cores=1\" --conf \"spark.kubernetes.memoryOverheadFactor=0.0\" local:///stackable/spark/examples/src/main/python/pi.py" + "containerdebug --output=/stackable/log/containerdebug-state.json --loop & /stackable/spark/bin/spark-submit --verbose --master k8s://https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS} --deploy-mode cluster --name pyspark-pi --conf spark.kubernetes.driver.podTemplateFile=/stackable/spark/driver-pod-templates/template.yaml --conf spark.kubernetes.executor.podTemplateFile=/stackable/spark/executor-pod-templates/template.yaml --conf spark.kubernetes.driver.podTemplateContainerName=spark --conf spark.kubernetes.executor.podTemplateContainerName=spark --conf spark.kubernetes.namespace=__NAMESPACE__ --conf spark.kubernetes.driver.container.image=oci.stackable.tech/sdp/spark-k8s:3.5.8-stackable0.0.0-dev --conf spark.kubernetes.executor.container.image=oci.stackable.tech/sdp/spark-k8s:3.5.8-stackable0.0.0-dev --conf spark.driver.defaultJavaOptions=-Dlog4j.configurationFile=/stackable/log_config/log4j2.properties --conf spark.driver.extraClassPath=/stackable/spark/extra-jars/* --conf spark.executor.defaultJavaOptions=-Dlog4j.configurationFile=/stackable/log_config/log4j2.properties --conf spark.executor.extraClassPath=/stackable/spark/extra-jars/* --conf spark.driver.extraJavaOptions=\"-Djava.security.properties=/stackable/log_config/security.properties\" --conf spark.executor.extraJavaOptions=\"-Djava.security.properties=/stackable/log_config/security.properties\" --conf spark.metrics.conf.\\*.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet --conf spark.metrics.conf.\\*.sink.prometheusServlet.path=/metrics/prometheus --conf spark.ui.prometheus.enabled=true --conf spark.sql.streaming.metricsEnabled=true --conf \"spark.driver.cores=2\" --conf \"spark.driver.memory=640m\" --conf \"spark.executor.cores=2\" --conf \"spark.executor.instances=1\" --conf \"spark.executor.memory=640m\" --conf \"spark.kubernetes.container.image.pullPolicy=IfNotPresent\" --conf \"spark.kubernetes.driver.limit.cores=2\" --conf \"spark.kubernetes.driver.request.cores=1\" --conf \"spark.kubernetes.executor.limit.cores=2\" --conf \"spark.kubernetes.executor.request.cores=1\" --conf \"spark.kubernetes.memoryOverheadFactor=0.0\" local:///stackable/spark/examples/src/main/python/pi.py" ], "command": [ "/bin/bash", diff --git a/tests/templates/kuttl/product-config-compat/fixtures/spark-connect-server-data.json b/tests/templates/kuttl/product-config-compat/fixtures/spark-connect-server-data.json index ac0b2aef..6f9f902a 100644 --- a/tests/templates/kuttl/product-config-compat/fixtures/spark-connect-server-data.json +++ b/tests/templates/kuttl/product-config-compat/fixtures/spark-connect-server-data.json @@ -1,6 +1,6 @@ { "metrics.properties": "*.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet\n*.sink.prometheusServlet.path=/metrics/prometheus\n", "security.properties": "networkaddress.cache.negative.ttl=0\nnetworkaddress.cache.ttl=30\n", - "spark-defaults.conf": "spark.driver.cores=3\nspark.driver.defaultJavaOptions=-Djava.security.properties\\=/stackable/spark/conf/security.properties\\ -Dlog4j.configurationFile\\=/stackable/log_config/log4j2.properties\\ -Dmy.custom.jvm.arg\\=customValue\nspark.driver.extraClassPath=/stackable/spark/extra-jars/*\\:/stackable/spark/connect/spark-connect-3.5.8.jar\nspark.driver.host=spark-connect-server-headless\nspark.executor.defaultJavaOptions=-Djava.security.properties\\=/stackable/spark/conf/security.properties\\ -Dlog4j.configurationFile\\=/stackable/log_config/log4j2.properties\nspark.executor.instances=3\nspark.executor.memory=1024M\nspark.executor.memoryOverhead=1m\nspark.kubernetes.authenticate.driver.serviceAccountName=spark-connect-serviceaccount\nspark.kubernetes.driver.container.image=oci.stackable.tech/sdp/spark-k8s\\:3.5.8-stackable0.0.0-dev\nspark.kubernetes.driver.pod.name=${env\\:HOSTNAME}\nspark.kubernetes.executor.container.image=oci.stackable.tech/sdp/spark-k8s\\:3.5.8-stackable0.0.0-dev\nspark.kubernetes.executor.limit.cores=1\nspark.kubernetes.executor.podTemplateContainerName=spark\nspark.kubernetes.executor.podTemplateFile=/stackable/spark/conf/template.yaml\nspark.kubernetes.executor.request.cores=1\nspark.kubernetes.namespace=__NAMESPACE__\nspark.metrics.conf=/stackable/spark/conf/metrics.properties\nspark.ui.prometheus.enabled=true\n", + "spark-defaults.conf": "spark.driver.cores=3\nspark.driver.defaultJavaOptions=-Djava.security.properties\\=/stackable/spark/conf/security.properties\\ -Dlog4j.configurationFile\\=/stackable/log_config/log4j2.properties\\ -Dmy.custom.jvm.arg\\=customValue\nspark.driver.extraClassPath=/stackable/spark/extra-jars/*\\:/stackable/spark/connect/spark-connect-3.5.8.jar\nspark.driver.host=spark-connect-server-headless\nspark.executor.defaultJavaOptions=-Djava.security.properties\\=/stackable/spark/conf/security.properties\\ -Dlog4j.configurationFile\\=/stackable/log_config/log4j2.properties\nspark.executor.instances=3\nspark.executor.memory=1024M\nspark.executor.memoryOverhead=1m\nspark.kubernetes.authenticate.driver.serviceAccountName=spark-connect-serviceaccount\nspark.kubernetes.container.image.pullPolicy=IfNotPresent\nspark.kubernetes.driver.container.image=oci.stackable.tech/sdp/spark-k8s\\:3.5.8-stackable0.0.0-dev\nspark.kubernetes.driver.pod.name=${env\\:HOSTNAME}\nspark.kubernetes.executor.container.image=oci.stackable.tech/sdp/spark-k8s\\:3.5.8-stackable0.0.0-dev\nspark.kubernetes.executor.limit.cores=1\nspark.kubernetes.executor.podTemplateContainerName=spark\nspark.kubernetes.executor.podTemplateFile=/stackable/spark/conf/template.yaml\nspark.kubernetes.executor.request.cores=1\nspark.kubernetes.namespace=__NAMESPACE__\nspark.metrics.conf=/stackable/spark/conf/metrics.properties\nspark.ui.prometheus.enabled=true\n", "template.yaml": "metadata:\n labels:\n app.kubernetes.io/component: executor\n app.kubernetes.io/instance: spark-connect\n app.kubernetes.io/managed-by: spark.stackable.tech_connect\n app.kubernetes.io/name: spark-connect\n app.kubernetes.io/version: 3.5.8-stackable0.0.0-dev\n stackable.tech/vendor: Stackable\nspec:\n affinity:\n podAntiAffinity:\n preferredDuringSchedulingIgnoredDuringExecution:\n - podAffinityTerm:\n labelSelector:\n matchLabels:\n app.kubernetes.io/component: executor\n app.kubernetes.io/instance: spark-connect\n app.kubernetes.io/name: spark-connect\n topologyKey: kubernetes.io/hostname\n weight: 70\n containers:\n - env:\n - name: CONTAINERDEBUG_LOG_DIRECTORY\n value: /stackable/log/containerdebug\n name: spark\n volumeMounts:\n - mountPath: /stackable/spark/conf\n name: config\n - mountPath: /stackable/log\n name: log\n - mountPath: /stackable/truststore\n name: stackable-truststore\n - mountPath: /stackable/log_config\n name: log-config\n enableServiceLinks: false\n securityContext:\n fsGroup: 1000\n volumes:\n - emptyDir:\n sizeLimit: 30Mi\n name: log\n - configMap:\n name: spark-connect-executor\n name: config\n - emptyDir: {}\n name: stackable-truststore\n - configMap:\n name: spark-connect-log-config\n name: log-config\n" }