Skip to content

Commit 87da728

Browse files
committed
Reapply "fix: make application templates namespaced (#694)" (#719) (#720)
* Reapply "fix: make application templates namespaced (#694)" (#719) This reverts commit e1b21e1. * update changelog
1 parent c7313a8 commit 87da728

7 files changed

Lines changed: 142 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ All notable changes to this project will be documented in this file.
3131
- Internal operator refactoring: introduce dereference() and validate() steps in the reconciler for spark application, spark connect and spark history server([#687]).
3232
- test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#689]).
3333
- Fix the `SparkApplication` CRD description, which incorrectly described it as a "Spark cluster stacklet" rather than a Spark application ([#705]).
34+
- BREAKING: make application templates namespaced instead of cluster wide objects ([#694]), reverted in [#719] and readded in [#720].
3435

3536
[#674]: https://github.com/stackabletech/spark-k8s-operator/pull/674
3637
[#679]: https://github.com/stackabletech/spark-k8s-operator/pull/679
@@ -39,11 +40,13 @@ All notable changes to this project will be documented in this file.
3940
[#687]: https://github.com/stackabletech/spark-k8s-operator/pull/687
4041
[#689]: https://github.com/stackabletech/spark-k8s-operator/pull/689
4142
[#692]: https://github.com/stackabletech/spark-k8s-operator/pull/692
43+
[#694]: https://github.com/stackabletech/spark-k8s-operator/pull/694
4244
[#696]: https://github.com/stackabletech/spark-k8s-operator/pull/696
4345
[#705]: https://github.com/stackabletech/spark-k8s-operator/pull/705
4446
[#708]: https://github.com/stackabletech/spark-k8s-operator/pull/708
4547
[#716]: https://github.com/stackabletech/spark-k8s-operator/pull/716
46-
[#XXXX]: https://github.com/stackabletech/spark-k8s-operator/pull/XXXX
48+
[#719]: https://github.com/stackabletech/spark-k8s-operator/pull/719
49+
[#720]: https://github.com/stackabletech/spark-k8s-operator/pull/720
4750

4851
## [26.3.0] - 2026-03-16
4952

Cargo.nix

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crate-hashes.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,23 @@ Spark application templates are used to define reusable configurations for Spark
55
When you have many applications with similar configurations, templates can help you avoid duplication by grouping common settings together.
66
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:
77

8-
1. Application templates are cluster wide resources, while Spark application resources are namespace-scoped.
9-
This means that application templates can be used across multiple namespaces, while Spark application resources are limited to the namespace they are created in.
8+
1. Application templates are namespace-scoped resources, just like Spark applications.
9+
This means that a SparkApplication can only reference templates from its own namespace.
1010
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.
1111
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.
1212
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.
1313
5. Application and template CRDs must have the exact same versions. Currently only `v1alpha1` is supported.
1414
15+
== Migrating from cluster-scoped templates
16+
17+
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.
18+
19+
If you are migrating from older installations where templates were treated as cluster-wide resources, account for the following:
20+
21+
1. Recreate each template in every namespace where SparkApplications use it.
22+
2. Keep template names consistent per namespace if you want the same application annotations to continue working.
23+
3. Cross-namespace template references are no longer resolved; templates and applications must be in the same namespace.
24+
4. Update GitOps/automation manifests to create templates as namespace-targeted resources before reconciling dependent SparkApplications.
1525
== Examples
1626

1727
Applications use `metadata.annotations` to reference application templates as shown below:

extra/crds.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4657,7 +4657,7 @@ spec:
46574657
shortNames:
46584658
- sparkapptemplate
46594659
singular: sparkapplicationtemplate
4660-
scope: Cluster
4660+
scope: Namespaced
46614661
versions:
46624662
- additionalPrinterColumns: []
46634663
name: v1alpha1

rust/operator-binary/src/crd/template_merger.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,47 @@ mod tests {
322322
assert_eq!(merged.spec.args, vec!["arg1", "arg2"]);
323323
}
324324

325+
#[test]
326+
fn test_deep_merge_metadata_namespace_overlay_wins() {
327+
let base = serde_yaml::from_str::<crate::crd::v1alpha1::SparkApplication>(indoc! {r#"
328+
---
329+
apiVersion: spark.stackable.tech/v1alpha1
330+
kind: SparkApplication
331+
metadata:
332+
name: base-app
333+
namespace: template-namespace
334+
spec:
335+
mode: cluster
336+
mainApplicationFile: base.py
337+
sparkImage:
338+
productVersion: "3.5.0"
339+
"#})
340+
.unwrap();
341+
342+
let overlay = serde_yaml::from_str::<crate::crd::v1alpha1::SparkApplication>(indoc! {r#"
343+
---
344+
apiVersion: spark.stackable.tech/v1alpha1
345+
kind: SparkApplication
346+
metadata:
347+
name: overlay-app
348+
namespace: app-namespace
349+
spec:
350+
mode: cluster
351+
mainApplicationFile: overlay.py
352+
sparkImage:
353+
productVersion: "3.5.1"
354+
"#})
355+
.unwrap();
356+
357+
let merged = deep_merge(&base, &overlay);
358+
359+
assert_eq!(
360+
merged.metadata.namespace,
361+
Some("app-namespace".to_string()),
362+
"overlay namespace should take precedence"
363+
);
364+
}
365+
325366
#[test]
326367
fn test_deep_merge_spark_conf() {
327368
let base = serde_yaml::from_str::<crate::crd::v1alpha1::SparkApplication>(indoc! {r#"

rust/operator-binary/src/crd/template_spec.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ pub enum Error {
4444
template_name: String,
4545
source: stackable_operator::kube::Error,
4646
},
47+
48+
#[snafu(display("object has no namespace"))]
49+
ObjectHasNoNamespace,
4750
}
4851

4952
#[versioned(
@@ -66,6 +69,7 @@ pub mod versioned {
6669
group = "spark.stackable.tech",
6770
plural = "sparkapptemplates",
6871
shortname = "sparkapptemplate",
72+
namespaced
6973
))]
7074
#[derive(Clone, CustomResource, Debug, Deserialize, JsonSchema, Serialize)]
7175
#[serde(rename_all = "camelCase")]
@@ -263,8 +267,10 @@ pub(crate) async fn merge_application_templates(
263267
// In the future if we support additional strategies in addition to "enforce",
264268
// this list might not be identical to the one in `merge_template_options`
265269
// because some objects might be missing.
270+
let namespace = spark_application_namespace(spark_application)?;
266271
let templates = resolve(
267272
client,
273+
namespace,
268274
&merge_template_options.template_names,
269275
merge_template_options.apply_strategy,
270276
)
@@ -317,16 +323,28 @@ pub(crate) async fn merge_application_templates(
317323
Ok(default_result)
318324
}
319325

326+
fn spark_application_namespace(
327+
spark_application: &super::v1alpha1::SparkApplication,
328+
) -> Result<&str, Error> {
329+
spark_application
330+
.metadata
331+
.namespace
332+
.as_deref()
333+
.ok_or(Error::ObjectHasNoNamespace)
334+
}
335+
320336
async fn resolve(
321337
client: &stackable_operator::client::Client,
338+
namespace: &str,
322339
template_names: &[String],
323340
apply_strategy: TemplateApplyStrategy,
324341
) -> Result<Vec<v1alpha1::SparkApplicationTemplate>, Error> {
325342
if template_names.is_empty() {
326343
return Ok(vec![]);
327344
}
328345

329-
let templates_api = Api::<v1alpha1::SparkApplicationTemplate>::all(client.as_kube_client());
346+
let templates_api =
347+
Api::<v1alpha1::SparkApplicationTemplate>::namespaced(client.as_kube_client(), namespace);
330348
let mut resolved_templates = Vec::new();
331349
for template_name in template_names {
332350
let template_res = templates_api
@@ -434,6 +452,53 @@ mod tests {
434452
assert!(options.template_names.is_empty());
435453
}
436454

455+
#[test]
456+
fn spark_application_namespace_returns_namespace() {
457+
let spark_application =
458+
serde_yaml::from_str::<crate::crd::v1alpha1::SparkApplication>(indoc! {r#"
459+
---
460+
apiVersion: spark.stackable.tech/v1alpha1
461+
kind: SparkApplication
462+
metadata:
463+
name: app-with-templates
464+
namespace: default
465+
spec:
466+
mode: cluster
467+
mainApplicationFile: local:///app.py
468+
sparkImage:
469+
productVersion: "3.5.8"
470+
"#})
471+
.unwrap();
472+
473+
assert_eq!(
474+
spark_application_namespace(&spark_application).unwrap(),
475+
"default"
476+
);
477+
}
478+
479+
#[test]
480+
fn spark_application_namespace_returns_error_when_missing() {
481+
let spark_application =
482+
serde_yaml::from_str::<crate::crd::v1alpha1::SparkApplication>(indoc! {r#"
483+
---
484+
apiVersion: spark.stackable.tech/v1alpha1
485+
kind: SparkApplication
486+
metadata:
487+
name: app-with-templates
488+
spec:
489+
mode: cluster
490+
mainApplicationFile: local:///app.py
491+
sparkImage:
492+
productVersion: "3.5.8"
493+
"#})
494+
.unwrap();
495+
496+
assert!(matches!(
497+
spark_application_namespace(&spark_application),
498+
Err(Error::ObjectHasNoNamespace)
499+
));
500+
}
501+
437502
impl RoundtripTestData for v1alpha1::SparkApplicationTemplateSpec {
438503
fn roundtrip_test_data() -> Vec<Self> {
439504
// SparkApplicationTemplateSpec is just a wrapper around SparkApplicationSpec

0 commit comments

Comments
 (0)