Skip to content

Commit 4335b15

Browse files
committed
extract OL structs into openlineage.rs
1 parent 87da728 commit 4335b15

3 files changed

Lines changed: 113 additions & 93 deletions

File tree

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

Lines changed: 2 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use crate::{
5252
SubmitConfig, SubmitConfigFragment, VolumeMounts,
5353
},
5454
},
55+
openlineage::{OpenLineageSpec, append_conf_csv},
5556
};
5657

5758
pub mod affinity;
@@ -76,7 +77,7 @@ pub enum Error {
7677
#[snafu(display("object defines no application artifact"))]
7778
ObjectHasNoArtifact,
7879

79-
#[snafu(display("object has no name"))]
80+
#[snafu(display("object has no name"), visibility(pub(crate)))]
8081
ObjectHasNoName,
8182

8283
#[snafu(display("application has no Spark image"))]
@@ -264,36 +265,6 @@ pub mod versioned {
264265
pub open_lineage: Option<OpenLineageSpec>,
265266
}
266267

267-
/// OpenLineage lineage emission for a [`SparkApplication`].
268-
///
269-
/// When enabled, the operator injects the OpenLineage Spark listener, points its HTTP transport
270-
/// at the backend resolved from the discovery ConfigMap, and sets a stable job name. All injected
271-
/// values are defaults: they can be overridden via `sparkConf`.
272-
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)]
273-
#[serde(rename_all = "camelCase")]
274-
pub struct OpenLineageSpec {
275-
/// Enable OpenLineage event emission. Defaults to `false` (nothing is injected).
276-
#[serde(default)]
277-
pub enabled: bool,
278-
279-
/// The OpenLineage namespace lineage is reported under.
280-
/// Defaults to the application's Kubernetes namespace (`metadata.namespace`).
281-
#[serde(default, skip_serializing_if = "Option::is_none")]
282-
pub namespace: Option<String>,
283-
284-
/// A stable OpenLineage job/application name. Setting this prevents fragmented run history
285-
/// (and the intermittent `unknown` job-name bug). If unset, the operator resolves it from
286-
/// `spark.app.name`, falling back to `metadata.name` (with a warning event).
287-
#[serde(default, skip_serializing_if = "Option::is_none")]
288-
pub app_name: Option<String>,
289-
290-
/// Name of the OpenLineage backend [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery).
291-
/// It must contain the key `ADDRESS` with the base URL of the OpenLineage backend
292-
/// (e.g. `http://marquez:5000`). Mirrors the `vectorAggregatorConfigMapName` field on this CRD.
293-
#[serde(default, skip_serializing_if = "Option::is_none")]
294-
pub config_map_name: Option<ConfigMapName>,
295-
}
296-
297268
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize, Merge)]
298269
pub struct ConfigOverrides {
299270
#[serde(default, rename = "spark-env.sh")]
@@ -304,35 +275,6 @@ pub mod versioned {
304275
}
305276
}
306277

307-
/// The resolved OpenLineage job/app name and where it came from.
308-
/// See [`v1alpha1::SparkApplication::resolved_openlineage_app_name`].
309-
#[derive(Clone, Debug, PartialEq)]
310-
pub struct ResolvedOpenLineageAppName {
311-
/// The resolved, non-blank name written to `spark.openlineage.appName`.
312-
pub name: String,
313-
/// `true` when the name fell back to `metadata.name`; the controller then emits a warning event
314-
/// because a per-run-unique name (e.g. an orchestrator-generated `-<timestamp>` suffix) fragments
315-
/// backend run history.
316-
pub from_metadata_name: bool,
317-
}
318-
319-
/// Appends `value` to a comma-separated `--conf` value in `submit_conf`, preserving any existing
320-
/// (e.g. user-supplied) entries and skipping `value` if it is already present. Used for the
321-
/// OpenLineage keys that must accumulate rather than clobber (`spark.extraListeners`, `spark.jars`).
322-
fn append_conf_csv(submit_conf: &mut BTreeMap<String, String>, key: &str, value: &str) {
323-
match submit_conf.get_mut(key) {
324-
Some(existing) if !existing.is_empty() => {
325-
if !existing.split(',').any(|entry| entry.trim() == value) {
326-
existing.push(',');
327-
existing.push_str(value);
328-
}
329-
}
330-
_ => {
331-
submit_conf.insert(key.to_string(), value.to_string());
332-
}
333-
}
334-
}
335-
336278
impl v1alpha1::SparkApplication {
337279
/// Returns if this [`SparkApplication`] has already created a Kubernetes Job doing the actual `spark-submit`.
338280
///
@@ -887,39 +829,6 @@ impl v1alpha1::SparkApplication {
887829
Ok(vec![submit_cmd.join(" ")])
888830
}
889831

890-
/// Resolves the stable OpenLineage job/app name and its provenance (MVP §5), in priority order:
891-
/// 1. `spec.openLineage.appName`, else
892-
/// 2. `spark.app.name` from `sparkConf`, else
893-
/// 3. `metadata.name` (a fallback the controller flags with a warning event, because a
894-
/// per-run-unique name would fragment backend run history).
895-
///
896-
/// Always yields a non-blank name — which is exactly what fixes the intermittent `unknown` bug.
897-
pub fn resolved_openlineage_app_name(&self) -> Result<ResolvedOpenLineageAppName, Error> {
898-
if let Some(app_name) = self
899-
.spec
900-
.open_lineage
901-
.as_ref()
902-
.and_then(|open_lineage| open_lineage.app_name.clone())
903-
{
904-
return Ok(ResolvedOpenLineageAppName {
905-
name: app_name,
906-
from_metadata_name: false,
907-
});
908-
}
909-
910-
if let Some(app_name) = self.spec.spark_conf.get("spark.app.name") {
911-
return Ok(ResolvedOpenLineageAppName {
912-
name: app_name.clone(),
913-
from_metadata_name: false,
914-
});
915-
}
916-
917-
Ok(ResolvedOpenLineageAppName {
918-
name: self.metadata.name.clone().context(ObjectHasNoNameSnafu)?,
919-
from_metadata_name: true,
920-
})
921-
}
922-
923832
pub fn env(
924833
&self,
925834
s3conn: &Option<s3::v1alpha1::ConnectionSpec>,

rust/operator-binary/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ mod config;
5151
mod connect;
5252
mod crd;
5353
mod history;
54+
mod openlineage;
5455
mod pod_driver_controller;
5556
mod product_logging;
5657
mod spark_k8s_controller;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//! OpenLineage lineage-emission types and helpers for [`SparkApplication`].
2+
//!
3+
//! [`SparkApplication`]: crate::crd::v1alpha1::SparkApplication
4+
5+
use std::collections::BTreeMap;
6+
7+
use serde::{Deserialize, Serialize};
8+
use snafu::OptionExt;
9+
use stackable_operator::{
10+
schemars::{self, JsonSchema},
11+
v2::types::kubernetes::ConfigMapName,
12+
};
13+
14+
use crate::crd::{Error, ObjectHasNoNameSnafu, v1alpha1};
15+
16+
/// OpenLineage lineage emission for a [`SparkApplication`].
17+
///
18+
/// When enabled, the operator injects the OpenLineage Spark listener, points its HTTP transport
19+
/// at the backend resolved from the discovery ConfigMap, and sets a stable job name. All injected
20+
/// values are defaults: they can be overridden via `sparkConf`.
21+
///
22+
/// [`SparkApplication`]: crate::crd::v1alpha1::SparkApplication
23+
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)]
24+
#[serde(rename_all = "camelCase")]
25+
pub struct OpenLineageSpec {
26+
/// Enable OpenLineage event emission. Defaults to `false` (nothing is injected).
27+
#[serde(default)]
28+
pub enabled: bool,
29+
30+
/// The OpenLineage namespace lineage is reported under.
31+
/// Defaults to the application's Kubernetes namespace (`metadata.namespace`).
32+
#[serde(default, skip_serializing_if = "Option::is_none")]
33+
pub namespace: Option<String>,
34+
35+
/// A stable OpenLineage job/application name. Setting this prevents fragmented run history
36+
/// (and the intermittent `unknown` job-name bug). If unset, the operator resolves it from
37+
/// `spark.app.name`, falling back to `metadata.name` (with a warning event).
38+
#[serde(default, skip_serializing_if = "Option::is_none")]
39+
pub app_name: Option<String>,
40+
41+
/// Name of the OpenLineage backend [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery).
42+
/// It must contain the key `ADDRESS` with the base URL of the OpenLineage backend
43+
/// (e.g. `http://marquez:5000`). Mirrors the `vectorAggregatorConfigMapName` field on this CRD.
44+
#[serde(default, skip_serializing_if = "Option::is_none")]
45+
pub config_map_name: Option<ConfigMapName>,
46+
}
47+
48+
/// The resolved OpenLineage job/app name and where it came from.
49+
/// See [`v1alpha1::SparkApplication::resolved_openlineage_app_name`].
50+
#[derive(Clone, Debug, PartialEq)]
51+
pub struct ResolvedOpenLineageAppName {
52+
/// The resolved, non-blank name written to `spark.openlineage.appName`.
53+
pub name: String,
54+
/// `true` when the name fell back to `metadata.name`; the controller then emits a warning event
55+
/// because a per-run-unique name (e.g. an orchestrator-generated `-<timestamp>` suffix) fragments
56+
/// backend run history.
57+
pub from_metadata_name: bool,
58+
}
59+
60+
/// Appends `value` to a comma-separated `--conf` value in `submit_conf`, preserving any existing
61+
/// (e.g. user-supplied) entries and skipping `value` if it is already present. Used for the
62+
/// OpenLineage keys that must accumulate rather than clobber (`spark.extraListeners`, `spark.jars`).
63+
pub(crate) fn append_conf_csv(submit_conf: &mut BTreeMap<String, String>, key: &str, value: &str) {
64+
match submit_conf.get_mut(key) {
65+
Some(existing) if !existing.is_empty() => {
66+
if !existing.split(',').any(|entry| entry.trim() == value) {
67+
existing.push(',');
68+
existing.push_str(value);
69+
}
70+
}
71+
_ => {
72+
submit_conf.insert(key.to_string(), value.to_string());
73+
}
74+
}
75+
}
76+
77+
impl v1alpha1::SparkApplication {
78+
/// Resolves the stable OpenLineage job/app name and its provenance (MVP §5), in priority order:
79+
/// 1. `spec.openLineage.appName`, else
80+
/// 2. `spark.app.name` from `sparkConf`, else
81+
/// 3. `metadata.name` (a fallback the controller flags with a warning event, because a
82+
/// per-run-unique name would fragment backend run history).
83+
///
84+
/// Always yields a non-blank name — which is exactly what fixes the intermittent `unknown` bug.
85+
pub fn resolved_openlineage_app_name(&self) -> Result<ResolvedOpenLineageAppName, Error> {
86+
if let Some(app_name) = self
87+
.spec
88+
.open_lineage
89+
.as_ref()
90+
.and_then(|open_lineage| open_lineage.app_name.clone())
91+
{
92+
return Ok(ResolvedOpenLineageAppName {
93+
name: app_name,
94+
from_metadata_name: false,
95+
});
96+
}
97+
98+
if let Some(app_name) = self.spec.spark_conf.get("spark.app.name") {
99+
return Ok(ResolvedOpenLineageAppName {
100+
name: app_name.clone(),
101+
from_metadata_name: false,
102+
});
103+
}
104+
105+
Ok(ResolvedOpenLineageAppName {
106+
name: self.metadata.name.clone().context(ObjectHasNoNameSnafu)?,
107+
from_metadata_name: true,
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)