Skip to content

Commit 1315e21

Browse files
committed
feat: add crd::openlineage module
1 parent 300422c commit 1315e21

6 files changed

Lines changed: 321 additions & 0 deletions

File tree

crates/stackable-operator/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Add `crd::openlineage` module with the `OpenLineageConnection` CRD (a reusable connection to an
10+
OpenLineage backend), an `InlineConnectionOrReference` wrapper with `resolve()`, and an embeddable
11+
`OpenLineageJob` type for operators ([#XXXX]).
12+
13+
[#XXXX]: https://github.com/stackabletech/operator-rs/pull/XXXX
14+
715
## [0.113.4] - 2026-07-09
816

917
### Changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
name: openlineageconnections.openlineage.stackable.tech
6+
spec:
7+
group: openlineage.stackable.tech
8+
names:
9+
categories: []
10+
kind: OpenLineageConnection
11+
plural: openlineageconnections
12+
shortNames: []
13+
singular: openlineageconnection
14+
scope: Namespaced
15+
versions:
16+
- additionalPrinterColumns: []
17+
name: v1alpha1
18+
schema:
19+
openAPIV3Schema:
20+
description: A reusable definition of a connection to an OpenLineage backend.
21+
properties:
22+
spec:
23+
description: |-
24+
OpenLineage connection definition as a resource.
25+
Learn more about [OpenLineage](https://openlineage.io/).
26+
properties:
27+
host:
28+
description: 'Host of the OpenLineage backend without any protocol or port. For example: `marquez`.'
29+
type: string
30+
port:
31+
description: 'Port the OpenLineage backend listens on. For example: `5000`.'
32+
format: uint16
33+
maximum: 65535.0
34+
minimum: 0.0
35+
type: integer
36+
tls:
37+
description: Use a TLS connection. If not specified no TLS will be used.
38+
nullable: true
39+
properties:
40+
verification:
41+
description: The verification method used to verify the certificates of the server and/or the client.
42+
oneOf:
43+
- required:
44+
- none
45+
- required:
46+
- server
47+
properties:
48+
none:
49+
description: Use TLS but don't verify certificates.
50+
type: object
51+
server:
52+
description: Use TLS and a CA certificate to verify the server.
53+
properties:
54+
caCert:
55+
description: CA cert to verify the server.
56+
oneOf:
57+
- required:
58+
- webPki
59+
- required:
60+
- secretClass
61+
properties:
62+
secretClass:
63+
description: |-
64+
Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate.
65+
Note that a SecretClass does not need to have a key but can also work with just a CA certificate,
66+
so if you got provided with a CA cert but don't have access to the key you can still use this method.
67+
type: string
68+
webPki:
69+
description: |-
70+
Use TLS and the CA certificates trusted by the common web browsers to verify the server.
71+
This can be useful when you e.g. use public AWS S3 or other public available services.
72+
type: object
73+
type: object
74+
required:
75+
- caCert
76+
type: object
77+
type: object
78+
required:
79+
- verification
80+
type: object
81+
required:
82+
- host
83+
- port
84+
type: object
85+
required:
86+
- spec
87+
title: OpenLineageConnection
88+
type: object
89+
served: true
90+
storage: true
91+
subresources: {}

crates/stackable-operator/src/crd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
77
pub mod authentication;
88
pub mod git_sync;
99
pub mod listener;
10+
pub mod openlineage;
1011
pub mod s3;
1112
pub mod scaler;
1213

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use kube::CustomResource;
2+
use schemars::JsonSchema;
3+
use serde::{Deserialize, Serialize};
4+
5+
use crate::{commons::tls_verification::TlsClientDetails, versioned::versioned};
6+
7+
mod v1alpha1_impl;
8+
9+
// FIXME (@Techassi): This should be versioned as well, but the macro cannot
10+
// handle new-type structs yet.
11+
/// Use this type in your operator!
12+
pub type ResolvedOpenLineageConnection = v1alpha1::OpenLineageConnectionSpec;
13+
14+
#[versioned(
15+
version(name = "v1alpha1"),
16+
crates(
17+
kube_core = "kube::core",
18+
k8s_openapi = "k8s_openapi",
19+
schemars = "schemars",
20+
)
21+
)]
22+
pub mod versioned {
23+
pub mod v1alpha1 {
24+
pub use v1alpha1_impl::OpenLineageError;
25+
}
26+
27+
/// OpenLineage connection definition as a resource.
28+
/// Learn more about [OpenLineage](https://openlineage.io/).
29+
#[versioned(crd(
30+
group = "openlineage.stackable.tech",
31+
kind = "OpenLineageConnection",
32+
plural = "openlineageconnections",
33+
doc = "A reusable definition of a connection to an OpenLineage backend.",
34+
namespaced
35+
))]
36+
#[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
37+
#[serde(rename_all = "camelCase")]
38+
pub struct OpenLineageConnectionSpec {
39+
/// Host of the OpenLineage backend without any protocol or port. For example: `marquez`.
40+
pub host: String,
41+
42+
/// Port the OpenLineage backend listens on. For example: `5000`.
43+
pub port: u16,
44+
45+
/// Use a TLS connection. If not specified no TLS will be used.
46+
/// When TLS server verification is configured, the transport uses `https` instead of `http`.
47+
#[serde(flatten)]
48+
pub tls: TlsClientDetails,
49+
}
50+
51+
/// An OpenLineage connection, either inlined or referenced by the name of an
52+
/// [`OpenLineageConnection`] resource in the same namespace.
53+
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
54+
#[serde(rename_all = "camelCase")]
55+
// TODO: This probably should be serde(untagged), but this would be a breaking change
56+
pub enum InlineConnectionOrReference {
57+
Inline(OpenLineageConnectionSpec),
58+
Reference(String),
59+
}
60+
61+
/// OpenLineage lineage-emission configuration for a single workload/application.
62+
///
63+
/// Embed this in an operator's workload spec to enable OpenLineage for that workload.
64+
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
65+
#[serde(rename_all = "camelCase")]
66+
pub struct OpenLineageJob {
67+
/// The OpenLineage backend connection, either inlined or referencing an
68+
/// `OpenLineageConnection` resource by name.
69+
pub connection: InlineConnectionOrReference,
70+
71+
/// The OpenLineage namespace lineage is reported under.
72+
/// If unset, operators typically default to the workload's Kubernetes namespace.
73+
#[serde(default, skip_serializing_if = "Option::is_none")]
74+
pub namespace: Option<String>,
75+
76+
/// A stable OpenLineage job/application name. Setting this prevents fragmented run history.
77+
/// If unset, operators resolve a name from workload-specific configuration.
78+
#[serde(default, skip_serializing_if = "Option::is_none")]
79+
pub app_name: Option<String>,
80+
}
81+
}
82+
83+
#[cfg(test)]
84+
impl stackable_versioned::test_utils::RoundtripTestData for v1alpha1::OpenLineageConnectionSpec {
85+
fn roundtrip_test_data() -> Vec<Self> {
86+
crate::utils::yaml_from_str_singleton_map(indoc::indoc! {"
87+
- host: marquez
88+
port: 5000
89+
- host: marquez
90+
port: 5000
91+
tls:
92+
verification:
93+
none: {}
94+
- host: marquez
95+
port: 5000
96+
tls:
97+
verification:
98+
server:
99+
caCert:
100+
secretClass: openlineage-cert
101+
"})
102+
.expect("Failed to parse OpenLineageConnectionSpec YAML")
103+
}
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use crate::{
109+
commons::tls_verification::{
110+
CaCert, Tls, TlsClientDetails, TlsServerVerification, TlsVerification,
111+
},
112+
crd::openlineage::v1alpha1::OpenLineageConnectionSpec,
113+
};
114+
115+
#[test]
116+
fn http_transport_url_without_tls() {
117+
let connection = OpenLineageConnectionSpec {
118+
host: "marquez".to_string(),
119+
port: 5000,
120+
tls: TlsClientDetails { tls: None },
121+
};
122+
123+
assert_eq!(connection.transport_url(), "http://marquez:5000");
124+
}
125+
126+
#[test]
127+
fn https_transport_url_with_server_verification() {
128+
let connection = OpenLineageConnectionSpec {
129+
host: "marquez".to_string(),
130+
port: 5000,
131+
tls: TlsClientDetails {
132+
tls: Some(Tls {
133+
verification: TlsVerification::Server(TlsServerVerification {
134+
ca_cert: CaCert::WebPki {},
135+
}),
136+
}),
137+
},
138+
};
139+
140+
assert_eq!(connection.transport_url(), "https://marquez:5000");
141+
}
142+
143+
#[test]
144+
fn http_transport_url_without_verification() {
145+
let connection = OpenLineageConnectionSpec {
146+
host: "marquez".to_string(),
147+
port: 5000,
148+
tls: TlsClientDetails {
149+
tls: Some(Tls {
150+
verification: TlsVerification::None {},
151+
}),
152+
},
153+
};
154+
155+
assert_eq!(connection.transport_url(), "http://marquez:5000");
156+
}
157+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use snafu::{ResultExt as _, Snafu};
2+
3+
use crate::{
4+
client::Client,
5+
crd::openlineage::{
6+
ResolvedOpenLineageConnection,
7+
v1alpha1::{InlineConnectionOrReference, OpenLineageConnection, OpenLineageConnectionSpec},
8+
},
9+
};
10+
11+
#[derive(Debug, Snafu)]
12+
pub enum OpenLineageError {
13+
#[snafu(display("failed to retrieve OpenLineage connection '{open_lineage_connection}'"))]
14+
RetrieveOpenLineageConnection {
15+
#[snafu(source(from(crate::client::Error, Box::new)))]
16+
source: Box<crate::client::Error>,
17+
open_lineage_connection: String,
18+
},
19+
}
20+
21+
impl OpenLineageConnectionSpec {
22+
/// Build the OpenLineage transport URL from this connection.
23+
///
24+
/// The scheme is `https` when TLS server verification is configured
25+
/// (`tls.verification.server`), otherwise `http`.
26+
pub fn transport_url(&self) -> String {
27+
let scheme = if self.tls.uses_tls_verification() {
28+
"https"
29+
} else {
30+
"http"
31+
};
32+
33+
format!(
34+
"{scheme}://{host}:{port}",
35+
host = self.host,
36+
port = self.port
37+
)
38+
}
39+
}
40+
41+
impl InlineConnectionOrReference {
42+
pub async fn resolve(
43+
self,
44+
client: &Client,
45+
namespace: &str,
46+
) -> Result<ResolvedOpenLineageConnection, OpenLineageError> {
47+
match self {
48+
Self::Inline(inline) => Ok(inline),
49+
Self::Reference(reference) => {
50+
let connection_spec = client
51+
.get::<OpenLineageConnection>(&reference, namespace)
52+
.await
53+
.context(RetrieveOpenLineageConnectionSnafu {
54+
open_lineage_connection: reference,
55+
})?
56+
.spec;
57+
58+
Ok(connection_spec)
59+
}
60+
}
61+
}
62+
}

crates/xtask/src/crd/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use stackable_operator::{
99
Listener, ListenerClass, ListenerClassVersion, ListenerVersion, PodListeners,
1010
PodListenersVersion,
1111
},
12+
openlineage::{OpenLineageConnection, OpenLineageConnectionVersion},
1213
s3::{S3Bucket, S3BucketVersion, S3Connection, S3ConnectionVersion},
1314
scaler::{Scaler, ScalerVersion},
1415
},
@@ -75,6 +76,7 @@ pub fn generate_preview() -> Result<(), Error> {
7576
write_crd!(path, AuthenticationClass, V1Alpha1);
7677
write_crd!(path, Listener, V1Alpha1);
7778
write_crd!(path, ListenerClass, V1Alpha1);
79+
write_crd!(path, OpenLineageConnection, V1Alpha1);
7880
write_crd!(path, PodListeners, V1Alpha1);
7981
write_crd!(path, S3Bucket, V1Alpha1);
8082
write_crd!(path, S3Connection, V1Alpha1);

0 commit comments

Comments
 (0)