diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe0f3f4..bfb2b8e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ All notable changes to this project will be documented in this file. discarded when the application references a SparkApplicationTemplate. The overrides of the template are now applied first and the ones of the SparkApplication on top of them, so an application can also remove a JVM argument that one of its templates added ([#745]). +- BREAKING: The connect and history-server listener PVC templates now carry unversioned labels. + Existing connect and history-server StatefulSets must be deleted once before the new operator can reconcile them ([#750]). [#721]: https://github.com/stackabletech/spark-k8s-operator/pull/721 [#727]: https://github.com/stackabletech/spark-k8s-operator/pull/727 @@ -36,6 +38,7 @@ All notable changes to this project will be documented in this file. [#744]: https://github.com/stackabletech/spark-k8s-operator/pull/744 [#745]: https://github.com/stackabletech/spark-k8s-operator/pull/745 [#746]: https://github.com/stackabletech/spark-k8s-operator/pull/746 +[#750]: https://github.com/stackabletech/spark-k8s-operator/pull/750 ## [26.7.0] - 2026-07-21 diff --git a/rust/operator-binary/src/connect/controller/build/server.rs b/rust/operator-binary/src/connect/controller/build/server.rs index 0135985c..c5b96310 100644 --- a/rust/operator-binary/src/connect/controller/build/server.rs +++ b/rust/operator-binary/src/connect/controller/build/server.rs @@ -287,6 +287,9 @@ pub(crate) fn build_stateful_set( )); } + let unversioned_recommended_labels = + validated.unversioned_recommended_labels(SparkConnectRole::Server); + // Add listener volume // Listener endpoints for the Webserver role will use persistent volumes // so that load balancers can hard-code the target addresses. This will @@ -295,7 +298,7 @@ pub(crate) fn build_stateful_set( let volume_claim_templates = Some(vec![ ListenerOperatorVolumeSourceBuilder::new( &ListenerReference::ListenerName(listener_name.to_string()), - &recommended_labels, + &unversioned_recommended_labels, ) .build_pvc(LISTENER_VOLUME_NAME.to_string()) .context(BuildListenerVolumeSnafu)?, diff --git a/rust/operator-binary/src/connect/controller/validate.rs b/rust/operator-binary/src/connect/controller/validate.rs index feb83e09..fa3fea9c 100644 --- a/rust/operator-binary/src/connect/controller/validate.rs +++ b/rust/operator-binary/src/connect/controller/validate.rs @@ -41,7 +41,7 @@ use crate::{ }, s3::ResolvedS3, }, - crd::constants::{CONTAINER_IMAGE_BASE_NAME, OPERATOR_NAME}, + crd::constants::{CONTAINER_IMAGE_BASE_NAME, OPERATOR_NAME, UNVERSIONED_PRODUCT_VERSION}, }; #[derive(Snafu, Debug)] @@ -179,6 +179,17 @@ impl ValidatedSparkConnectServer { self.recommended_labels_with(&self.product_version, role_name, role_group_name) } + /// Recommended labels with a fixed placeholder version, for objects that live in immutable + /// fields (e.g. the listener PVC in the StatefulSet's `volumeClaimTemplates`) and therefore + /// must not carry labels that change on upgrade. + pub fn unversioned_recommended_labels(&self, role: SparkConnectRole) -> Labels { + self.recommended_labels_with( + &UNVERSIONED_PRODUCT_VERSION, + &role.into(), + &DEFAULT_SPARK_CONNECT_ROLE_GROUP, + ) + } + /// Recommended labels for a resource of the given role. pub fn recommended_labels(&self, role: SparkConnectRole) -> Labels { self.recommended_labels_for(&role.into(), &DEFAULT_SPARK_CONNECT_ROLE_GROUP) diff --git a/rust/operator-binary/src/crd/constants.rs b/rust/operator-binary/src/crd/constants.rs index efef0f14..4e429868 100644 --- a/rust/operator-binary/src/crd/constants.rs +++ b/rust/operator-binary/src/crd/constants.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, str::FromStr}; use const_format::concatcp; use stackable_operator::{ memory::{BinaryMultiple, MemoryQuantity}, - v2::types::{common::Port, kubernetes::VolumeName}, + v2::types::{common::Port, kubernetes::VolumeName, operator::ProductVersion}, }; pub const APP_NAME: &str = "spark-k8s"; @@ -101,6 +101,10 @@ stackable_operator::constant!(pub LISTENER_VOLUME_NAME: VolumeName = "listener") pub const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; pub const DEFAULT_LISTENER_CLASS: &str = "cluster-internal"; +// Placeholder product version used for labels on PVC templates, which cannot be modified once +// deployed. A constant value keeps the labels stable across version upgrades. +stackable_operator::constant!(pub UNVERSIONED_PRODUCT_VERSION: ProductVersion = "none"); + pub const DEFAULT_SUBMIT_JOB_RETRY_ON_FAILURE_COUNT: u16 = 0; /// The JVM `security.properties` entries the operator sets by default (DNS cache TTLs). diff --git a/rust/operator-binary/src/history/controller/build/resource/statefulset.rs b/rust/operator-binary/src/history/controller/build/resource/statefulset.rs index 9c30c626..17aaec73 100644 --- a/rust/operator-binary/src/history/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/history/controller/build/resource/statefulset.rs @@ -213,6 +213,8 @@ pub(crate) fn build_stateful_set( .context(AddVolumeMountSnafu)? .build(); + let unversioned_recommended_labels = validated.unversioned_recommended_labels(role_group_name); + // Add listener volume // Listener endpoints for the Webserver role will use persistent volumes // so that load balancers can hard-code the target addresses. This will @@ -224,7 +226,7 @@ pub(crate) fn build_stateful_set( .parse() .expect("the group listener name is a valid ListenerName"), ), - &recommended_labels, + &unversioned_recommended_labels, &LISTENER_VOLUME_NAME_PVC, )]); diff --git a/rust/operator-binary/src/history/controller/validate.rs b/rust/operator-binary/src/history/controller/validate.rs index b0aaaab9..8c83759a 100644 --- a/rust/operator-binary/src/history/controller/validate.rs +++ b/rust/operator-binary/src/history/controller/validate.rs @@ -41,7 +41,7 @@ use crate::{ crd::{ constants::{ CONTAINER_IMAGE_BASE_NAME, HISTORY_APP_NAME, HISTORY_CONTROLLER_NAME, - HISTORY_ROLE_NAME, OPERATOR_NAME, + HISTORY_ROLE_NAME, OPERATOR_NAME, UNVERSIONED_PRODUCT_VERSION, }, history::{HistoryConfig, HistoryConfigFragment, SparkHistoryServerContainer, v1alpha1}, logdir::ResolvedLogDir, @@ -231,6 +231,17 @@ impl ValidatedSparkHistoryServer { self.recommended_labels_with(&self.product_version, role_name, role_group_name) } + /// Recommended labels with a fixed placeholder version, for objects that live in immutable + /// fields (e.g. the listener PVC in the StatefulSet's `volumeClaimTemplates`) and therefore + /// must not carry labels that change on upgrade. + pub fn unversioned_recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { + self.recommended_labels_with( + &UNVERSIONED_PRODUCT_VERSION, + &Self::role_name(), + role_group_name, + ) + } + fn recommended_labels_with( &self, product_version: &ProductVersion, diff --git a/tests/templates/kuttl/smoke/50-assert.yaml b/tests/templates/kuttl/smoke/50-assert.yaml index 1319f33a..a6f2e4ca 100644 --- a/tests/templates/kuttl/smoke/50-assert.yaml +++ b/tests/templates/kuttl/smoke/50-assert.yaml @@ -11,5 +11,11 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - script: | + # Wait for the spark-submit pod: the image pull can take minutes and + # kuttl would otherwise retry (and log) this whole script every second. + kubectl wait -n $NAMESPACE pod \ + --selector batch.kubernetes.io/job-name=spark-pi-s3-1 \ + --for=jsonpath='{.status.phase}'=Running \ + --timeout=280s > /dev/null 2>&1 || exit 1 SPARK_SUBMIT_POD=$(kubectl get -n $NAMESPACE pods --field-selector=status.phase=Running --selector batch.kubernetes.io/job-name=spark-pi-s3-1 -o jsonpath='{.items[0].metadata.name}') kubectl exec -n $NAMESPACE --container spark-submit $SPARK_SUBMIT_POD -- cat /stackable/log/containerdebug-state.json | jq --exit-status '"valid JSON"'