From ac6c01fa9c14efbba1565fb933c41b646f02ec6f Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:49:26 +0200 Subject: [PATCH 1/2] Reapply "fix: make application templates namespaced (#694)" (#719) This reverts commit e1b21e163ab5e1f68089b329a3ecf5922c2cf4cd. --- CHANGELOG.md | 2 + Cargo.nix | 18 ++--- crate-hashes.json | 18 ++--- .../pages/usage-guide/app_templates.adoc | 14 +++- extra/crds.yaml | 2 +- .../src/crd/template_merger.rs | 41 ++++++++++++ rust/operator-binary/src/crd/template_spec.rs | 67 ++++++++++++++++++- 7 files changed, 140 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58624f6a..e198c570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file. - Internal operator refactoring: introduce dereference() and validate() steps in the reconciler for spark application, spark connect and spark history server([#687]). - test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#689]). - Fix the `SparkApplication` CRD description, which incorrectly described it as a "Spark cluster stacklet" rather than a Spark application ([#705]). +- BREAKING: make application templates namespaced instead of cluster wide objects ([#694]). [#674]: https://github.com/stackabletech/spark-k8s-operator/pull/674 [#679]: https://github.com/stackabletech/spark-k8s-operator/pull/679 @@ -38,6 +39,7 @@ All notable changes to this project will be documented in this file. [#687]: https://github.com/stackabletech/spark-k8s-operator/pull/687 [#689]: https://github.com/stackabletech/spark-k8s-operator/pull/689 [#692]: https://github.com/stackabletech/spark-k8s-operator/pull/692 +[#694]: https://github.com/stackabletech/spark-k8s-operator/pull/694 [#696]: https://github.com/stackabletech/spark-k8s-operator/pull/696 [#705]: https://github.com/stackabletech/spark-k8s-operator/pull/705 [#708]: https://github.com/stackabletech/spark-k8s-operator/pull/708 diff --git a/Cargo.nix b/Cargo.nix index 4bb9f18e..fd10413f 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -5109,7 +5109,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; libName = "k8s_version"; authors = [ @@ -9893,7 +9893,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; libName = "stackable_certs"; authors = [ @@ -9996,7 +9996,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; libName = "stackable_operator"; authors = [ @@ -10195,7 +10195,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -10230,7 +10230,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; libName = "stackable_shared"; authors = [ @@ -10417,7 +10417,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; libName = "stackable_telemetry"; authors = [ @@ -10527,7 +10527,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; libName = "stackable_versioned"; authors = [ @@ -10577,7 +10577,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10645,7 +10645,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; + sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; }; libName = "stackable_webhook"; authors = [ diff --git a/crate-hashes.json b/crate-hashes.json index 77adf52a..cd3917c3 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#k8s-version@0.1.3": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-certs@0.4.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator-derive@0.3.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator@0.113.3": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-shared@0.1.2": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-telemetry@0.6.5": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned-macros@0.11.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned@0.11.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-webhook@0.9.2": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#k8s-version@0.1.3": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-certs@0.4.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator-derive@0.3.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator@0.113.3": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-shared@0.1.2": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-telemetry@0.6.5": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned-macros@0.11.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned@0.11.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-webhook@0.9.2": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file diff --git a/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc b/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc index 67a289fb..be8ebb9f 100644 --- a/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc +++ b/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc @@ -5,13 +5,23 @@ Spark application templates are used to define reusable configurations for Spark When you have many applications with similar configurations, templates can help you avoid duplication by grouping common settings together. Application templates are available for the `v1alpha1` version of the SparkApplication custom resource and share the exact same structure as the SparkApplication resource, but with some differences in the way the operator handles them: -1. Application templates are cluster wide resources, while Spark application resources are namespace-scoped. - This means that application templates can be used across multiple namespaces, while Spark application resources are limited to the namespace they are created in. +1. Application templates are namespace-scoped resources, just like Spark applications. + This means that a SparkApplication can only reference templates from its own namespace. 2. Application templates are not reconciled by the operator, but must be referenced from a SparkApplication resource to be applied. This means that changes to an application template will not automatically trigger updates to SparkApplication resources that reference it. 3. An application can reference multiple application templates, and the settings from these templates will be merged together. The merging order of the templates is indicated by their index in the reference list. The application fields have the highest precedence and will override any conflicting settings from the templates. This allows you to have a base template with common settings and then override specific settings in the application resource as needed. 4. Application template references are immutable in the sense that once applied to an application they cannot be changed again. Currently templates are applied upon the creation of the application, and any changes to the template references after that will be ignored. 5. Application and template CRDs must have the exact same versions. Currently only `v1alpha1` is supported. +== Migrating from cluster-scoped templates + +IMPORTANT: Application templates used to be cluster wide resources when they were first released. This was a mistake. Many users do not have the access rights to create cluster scoped resources and so the templates are now namespace scoped. + +If you are migrating from older installations where templates were treated as cluster-wide resources, account for the following: + +1. Recreate each template in every namespace where SparkApplications use it. +2. Keep template names consistent per namespace if you want the same application annotations to continue working. +3. Cross-namespace template references are no longer resolved; templates and applications must be in the same namespace. +4. Update GitOps/automation manifests to create templates as namespace-targeted resources before reconciling dependent SparkApplications. == Examples Applications use `metadata.annotations` to reference application templates as shown below: diff --git a/extra/crds.yaml b/extra/crds.yaml index a21ac40c..ab28308f 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -4621,7 +4621,7 @@ spec: shortNames: - sparkapptemplate singular: sparkapplicationtemplate - scope: Cluster + scope: Namespaced versions: - additionalPrinterColumns: [] name: v1alpha1 diff --git a/rust/operator-binary/src/crd/template_merger.rs b/rust/operator-binary/src/crd/template_merger.rs index 1ee7f21c..7f8afbba 100644 --- a/rust/operator-binary/src/crd/template_merger.rs +++ b/rust/operator-binary/src/crd/template_merger.rs @@ -317,6 +317,47 @@ mod tests { assert_eq!(merged.spec.args, vec!["arg1", "arg2"]); } + #[test] + fn test_deep_merge_metadata_namespace_overlay_wins() { + let base = serde_yaml::from_str::(indoc! {r#" + --- + apiVersion: spark.stackable.tech/v1alpha1 + kind: SparkApplication + metadata: + name: base-app + namespace: template-namespace + spec: + mode: cluster + mainApplicationFile: base.py + sparkImage: + productVersion: "3.5.0" + "#}) + .unwrap(); + + let overlay = serde_yaml::from_str::(indoc! {r#" + --- + apiVersion: spark.stackable.tech/v1alpha1 + kind: SparkApplication + metadata: + name: overlay-app + namespace: app-namespace + spec: + mode: cluster + mainApplicationFile: overlay.py + sparkImage: + productVersion: "3.5.1" + "#}) + .unwrap(); + + let merged = deep_merge(&base, &overlay); + + assert_eq!( + merged.metadata.namespace, + Some("app-namespace".to_string()), + "overlay namespace should take precedence" + ); + } + #[test] fn test_deep_merge_spark_conf() { let base = serde_yaml::from_str::(indoc! {r#" diff --git a/rust/operator-binary/src/crd/template_spec.rs b/rust/operator-binary/src/crd/template_spec.rs index da5fdd05..7312abe3 100644 --- a/rust/operator-binary/src/crd/template_spec.rs +++ b/rust/operator-binary/src/crd/template_spec.rs @@ -44,6 +44,9 @@ pub enum Error { template_name: String, source: stackable_operator::kube::Error, }, + + #[snafu(display("object has no namespace"))] + ObjectHasNoNamespace, } #[versioned( @@ -66,6 +69,7 @@ pub mod versioned { group = "spark.stackable.tech", plural = "sparkapptemplates", shortname = "sparkapptemplate", + namespaced ))] #[derive(Clone, CustomResource, Debug, Deserialize, JsonSchema, Serialize)] #[serde(rename_all = "camelCase")] @@ -263,8 +267,10 @@ pub(crate) async fn merge_application_templates( // In the future if we support additional strategies in addition to "enforce", // this list might not be identical to the one in `merge_template_options` // because some objects might be missing. + let namespace = spark_application_namespace(spark_application)?; let templates = resolve( client, + namespace, &merge_template_options.template_names, merge_template_options.apply_strategy, ) @@ -317,8 +323,19 @@ pub(crate) async fn merge_application_templates( Ok(default_result) } +fn spark_application_namespace( + spark_application: &super::v1alpha1::SparkApplication, +) -> Result<&str, Error> { + spark_application + .metadata + .namespace + .as_deref() + .ok_or(Error::ObjectHasNoNamespace) +} + async fn resolve( client: &stackable_operator::client::Client, + namespace: &str, template_names: &[String], apply_strategy: TemplateApplyStrategy, ) -> Result, Error> { @@ -326,7 +343,8 @@ async fn resolve( return Ok(vec![]); } - let templates_api = Api::::all(client.as_kube_client()); + let templates_api = + Api::::namespaced(client.as_kube_client(), namespace); let mut resolved_templates = Vec::new(); for template_name in template_names { let template_res = templates_api @@ -434,6 +452,53 @@ mod tests { assert!(options.template_names.is_empty()); } + #[test] + fn spark_application_namespace_returns_namespace() { + let spark_application = + serde_yaml::from_str::(indoc! {r#" + --- + apiVersion: spark.stackable.tech/v1alpha1 + kind: SparkApplication + metadata: + name: app-with-templates + namespace: default + spec: + mode: cluster + mainApplicationFile: local:///app.py + sparkImage: + productVersion: "3.5.8" + "#}) + .unwrap(); + + assert_eq!( + spark_application_namespace(&spark_application).unwrap(), + "default" + ); + } + + #[test] + fn spark_application_namespace_returns_error_when_missing() { + let spark_application = + serde_yaml::from_str::(indoc! {r#" + --- + apiVersion: spark.stackable.tech/v1alpha1 + kind: SparkApplication + metadata: + name: app-with-templates + spec: + mode: cluster + mainApplicationFile: local:///app.py + sparkImage: + productVersion: "3.5.8" + "#}) + .unwrap(); + + assert!(matches!( + spark_application_namespace(&spark_application), + Err(Error::ObjectHasNoNamespace) + )); + } + impl RoundtripTestData for v1alpha1::SparkApplicationTemplateSpec { fn roundtrip_test_data() -> Vec { // SparkApplicationTemplateSpec is just a wrapper around SparkApplicationSpec From 04d06d5ab19ebee18f46bb458db661d73b82f2c5 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:51:36 +0200 Subject: [PATCH 2/2] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e198c570..efaf4921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ All notable changes to this project will be documented in this file. - Internal operator refactoring: introduce dereference() and validate() steps in the reconciler for spark application, spark connect and spark history server([#687]). - test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#689]). - Fix the `SparkApplication` CRD description, which incorrectly described it as a "Spark cluster stacklet" rather than a Spark application ([#705]). -- BREAKING: make application templates namespaced instead of cluster wide objects ([#694]). +- BREAKING: make application templates namespaced instead of cluster wide objects ([#694]), reverted in [#719] and readded in [#720]. [#674]: https://github.com/stackabletech/spark-k8s-operator/pull/674 [#679]: https://github.com/stackabletech/spark-k8s-operator/pull/679 @@ -44,6 +44,8 @@ All notable changes to this project will be documented in this file. [#705]: https://github.com/stackabletech/spark-k8s-operator/pull/705 [#708]: https://github.com/stackabletech/spark-k8s-operator/pull/708 [#716]: https://github.com/stackabletech/spark-k8s-operator/pull/716 +[#719]: https://github.com/stackabletech/spark-k8s-operator/pull/719 +[#720]: https://github.com/stackabletech/spark-k8s-operator/pull/720 ## [26.3.0] - 2026-03-16