-
-
Notifications
You must be signed in to change notification settings - Fork 4
fix: use sparkImage.pullPolicy in all containers #764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should mention the corner case where |
||
|
|
||
| NOTE: Under a mutable image tag, `IfNotPresent` lets a node serve a stale cached image, so `Always` is the safe option for driver and executors to match the submit pod. | ||
| Even that is not guaranteed, though, 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: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The executor properties are merged last, so a user override for this property will be silently overridden (the other properties added in this function are "internal"). This should go before user overrides are applied e.g. in server_properties? |
||
| 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(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: if we passed |
||
| 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()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also assert the expected value for the excluded container, so that it checks/guards that the template container does not carry its own pull policy? (and so we can't change that silently in the future) |
||
| .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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to duplicate the ConnectionSpec part of connection_fixture: maybe consolidate them (e.g. move connection_fixture to here and have tls_connection call it)? |
||
| 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<Option<stackable_operator::k8s_openapi::api::core::v1::Container>, Error> { | ||
| image: &ResolvedProductImage, | ||
| ) -> Result<Option<Container>, 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) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the history server excluded? If so, maybe make that clear here. Also Connect Server takes
spec.imageand notsparkImage(maybe that is implied but we could make it clearer).