Skip to content

Commit b326be3

Browse files
authored
fix: make application templates namespaced (#694)
1 parent 98fba78 commit b326be3

4 files changed

Lines changed: 120 additions & 4 deletions

File tree

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
@@ -4639,7 +4639,7 @@ spec:
46394639
shortNames:
46404640
- sparkapptemplate
46414641
singular: sparkapplicationtemplate
4642-
scope: Cluster
4642+
scope: Namespaced
46434643
versions:
46444644
- additionalPrinterColumns: []
46454645
name: v1alpha1

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

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

315+
#[test]
316+
fn test_deep_merge_metadata_namespace_overlay_wins() {
317+
let base = serde_yaml::from_str::<crate::crd::v1alpha1::SparkApplication>(indoc! {r#"
318+
---
319+
apiVersion: spark.stackable.tech/v1alpha1
320+
kind: SparkApplication
321+
metadata:
322+
name: base-app
323+
namespace: template-namespace
324+
spec:
325+
mode: cluster
326+
mainApplicationFile: base.py
327+
sparkImage:
328+
productVersion: "3.5.0"
329+
"#})
330+
.unwrap();
331+
332+
let overlay = serde_yaml::from_str::<crate::crd::v1alpha1::SparkApplication>(indoc! {r#"
333+
---
334+
apiVersion: spark.stackable.tech/v1alpha1
335+
kind: SparkApplication
336+
metadata:
337+
name: overlay-app
338+
namespace: app-namespace
339+
spec:
340+
mode: cluster
341+
mainApplicationFile: overlay.py
342+
sparkImage:
343+
productVersion: "3.5.1"
344+
"#})
345+
.unwrap();
346+
347+
let merged = deep_merge(&base, &overlay);
348+
349+
assert_eq!(
350+
merged.metadata.namespace,
351+
Some("app-namespace".to_string()),
352+
"overlay namespace should take precedence"
353+
);
354+
}
355+
315356
#[test]
316357
fn test_deep_merge_spark_conf() {
317358
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(
@@ -65,6 +68,7 @@ pub mod versioned {
6568
group = "spark.stackable.tech",
6669
plural = "sparkapptemplates",
6770
shortname = "sparkapptemplate",
71+
namespaced
6872
))]
6973
#[derive(Clone, CustomResource, Debug, Deserialize, JsonSchema, Serialize)]
7074
#[serde(rename_all = "camelCase")]
@@ -262,8 +266,10 @@ pub(crate) async fn merge_application_templates(
262266
// In the future if we support additional strategies in addition to "enforce",
263267
// this list might not be identical to the one in `merge_template_options`
264268
// because some objects might be missing.
269+
let namespace = spark_application_namespace(spark_application)?;
265270
let templates = resolve(
266271
client,
272+
namespace,
267273
&merge_template_options.template_names,
268274
merge_template_options.apply_strategy,
269275
)
@@ -316,16 +322,28 @@ pub(crate) async fn merge_application_templates(
316322
Ok(default_result)
317323
}
318324

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

328-
let templates_api = Api::<v1alpha1::SparkApplicationTemplate>::all(client.as_kube_client());
345+
let templates_api =
346+
Api::<v1alpha1::SparkApplicationTemplate>::namespaced(client.as_kube_client(), namespace);
329347
let mut resolved_templates = Vec::new();
330348
for template_name in template_names {
331349
let template_res = templates_api
@@ -433,6 +451,53 @@ mod tests {
433451
assert!(options.template_names.is_empty());
434452
}
435453

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

0 commit comments

Comments
 (0)