Skip to content

Commit f3df803

Browse files
sbernauermaltesanderTechassi
authored
feat: Add generic database connection mechanism (#1163)
* WIP * postgres -> postgresql * Store username and password envs separately * Better name modules * Remove leftover code * Switch to config-utils templating * Add TODO marker * Add MySQL * Improve Derby JDBC driver * empty commit * Auto-create Derby database * Update module structure * clippy * cargo fmt * Implement CeleryConnection for Postgresql * Support specifying the templating mechanism * fix: Use $VAR instead of ${VAR} (for Airflow) * Revert "fix: Use $VAR instead of ${VAR} (for Airflow)" This reverts commit 8e05224. It was actually mot needed :) * Add lots of docs * Improve docs * Apply suggestions from code review Co-authored-by: Malte Sander <malte.sander.it@gmail.com> * refactor!: Take IntoIterator<Item = EnvVar> instead * Remove too specific JDBC * JDBC -> Jdbc and SQLAlchemy -> SqlAlchemy * Update crates/stackable-operator/src/databases/databases/derby.rs Co-authored-by: Techassi <git@techassi.dev> * fix compilation * refactor: Let connection_parameters_as_url_query_parameters return Option<String> * Put "org.postgresql.Driver" behind POSTGRES_JDBC_DRIVER_CLASS constant * refactor: Switch derby location from String to PathBuf * Clarify what add_to_container does * Remove bullet points * Add docs on TemplatingMechanism * Rename "databases" module to "database_connections" * Explain context(false) and improve error messages * Add TODO on connection param escaping * Add comment on airflow-op switching to config files * Address field name feedback; URI -> URL * Leftovers I forgot to commit * Rename env_var_from_secret to env_var_with_value_from_secret * Add comment on snafu error * Move Derby JDBC driver into consant * Update CHANGELOG.md --------- Co-authored-by: Malte Sander <malte.sander.it@gmail.com> Co-authored-by: Techassi <git@techassi.dev>
1 parent a4a204b commit f3df803

21 files changed

Lines changed: 1353 additions & 3 deletions

File tree

crates/stackable-operator/CHANGELOG.md

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

55
## [Unreleased]
66

7+
### Added
8+
9+
- Add generic database connection mechanism ([#1163]).
10+
11+
### Changed
12+
13+
- BREAKING: Change signature of `ContainerBuilder::add_env_vars` from `Vec<EnvVar>` to `IntoIterator<Item = EnvVar>` ([#1163]).
14+
15+
[#1163]: https://github.com/stackabletech/operator-rs/pull/1163
16+
717
## [0.109.0] - 2026-04-07
818

919
### Added

crates/stackable-operator/crds/DummyCluster.yaml

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,208 @@ spec:
8282
and `stopped` will take no effect until `reconciliationPaused` is set to false or removed.
8383
type: boolean
8484
type: object
85+
databaseConnection:
86+
oneOf:
87+
- required:
88+
- postgresql
89+
- required:
90+
- mysql
91+
- required:
92+
- derby
93+
- required:
94+
- redis
95+
- required:
96+
- genericJdbc
97+
- required:
98+
- genericSqlAlchemy
99+
- required:
100+
- genericCelery
101+
properties:
102+
derby:
103+
description: |-
104+
Connection settings for an embedded [Apache Derby](https://db.apache.org/derby/) database.
105+
106+
Derby is an embedded, file-based Java database engine that requires no separate server process.
107+
It is typically used for development, testing, or as a lightweight metastore backend (e.g. for
108+
Apache Hive).
109+
properties:
110+
location:
111+
description: |-
112+
Path on the filesystem where Derby stores its database files.
113+
114+
If not specified, defaults to `/tmp/derby/{unique_database_name}/derby.db`.
115+
The `{unique_database_name}` part is automatically handled by the operator and is added to
116+
prevent clashing database files. The `create=true` flag is always appended to the JDBC URL,
117+
so the database is created automatically if it does not yet exist at this location.
118+
nullable: true
119+
type: string
120+
type: object
121+
genericCelery:
122+
description: |-
123+
A generic Celery database connection for broker or result backend types not covered by a
124+
dedicated variant.
125+
126+
Use this when you need a Celery-compatible connection that does not have a first-class
127+
connection type. The complete connection URL is read from a Secret, giving the user full
128+
control over the connection string.
129+
properties:
130+
connectionUrlSecretName:
131+
description: The name of the Secret that contains an `connectionUrl` key with the complete Celery URL.
132+
type: string
133+
required:
134+
- connectionUrlSecretName
135+
type: object
136+
genericJdbc:
137+
description: |-
138+
A generic JDBC database connection for database types not covered by a dedicated variant.
139+
140+
Use this when you need to connect to a JDBC-compatible database that does not have a
141+
first-class connection type. You are responsible for providing the correct driver class name
142+
and a fully-formed JDBC URL as well as providing the needed classes on the Java classpath.
143+
properties:
144+
credentialsSecretName:
145+
description: |-
146+
Name of a Secret containing the `username` and `password` keys used to authenticate
147+
against the database.
148+
type: string
149+
driver:
150+
description: |-
151+
Fully-qualified Java class name of the JDBC driver, e.g. `org.postgresql.Driver` or
152+
`com.mysql.jdbc.Driver`. The driver JAR must be provided by you on the classpath.
153+
type: string
154+
url:
155+
description: |-
156+
The JDBC connection URL, e.g. `jdbc:postgresql://my-host:5432/mydb`. Credentials must
157+
not be embedded in this URL; they are instead injected via environment variables sourced
158+
from `credentials_secret`.
159+
format: uri
160+
type: string
161+
required:
162+
- credentialsSecretName
163+
- driver
164+
- url
165+
type: object
166+
genericSqlAlchemy:
167+
description: |-
168+
A generic SQLAlchemy database connection for database types not covered by a dedicated variant.
169+
170+
Use this when you need to connect to a SQLAlchemy-compatible database that does not have a
171+
first-class connection type. The complete connection URL is read from a Secret, giving the user
172+
full control over the connection string including any driver-specific options.
173+
properties:
174+
connectionUrlSecretName:
175+
description: The name of the Secret that contains an `connectionUrl` key with the complete SQLAlchemy URL.
176+
type: string
177+
required:
178+
- connectionUrlSecretName
179+
type: object
180+
mysql:
181+
description: Connection settings for a [MySQL](https://www.mysql.com/) database.
182+
properties:
183+
credentialsSecretName:
184+
description: |-
185+
Name of a Secret containing the `username` and `password` keys used to authenticate
186+
against the MySQL server.
187+
type: string
188+
database:
189+
description: Name of the database (schema) to connect to.
190+
type: string
191+
host:
192+
description: Hostname or IP address of the MySQL server.
193+
type: string
194+
parameters:
195+
additionalProperties:
196+
type: string
197+
default: {}
198+
description: |-
199+
Additional map of connection parameters to append to the connection URL. The given
200+
`HashMap<String, String>` will be converted to query parameters in the form of
201+
`?param1=value1&param2=value2`.
202+
type: object
203+
port:
204+
default: 3306
205+
description: Port the MySQL server is listening on. Defaults to `3306`.
206+
format: uint16
207+
maximum: 65535.0
208+
minimum: 0.0
209+
type: integer
210+
required:
211+
- credentialsSecretName
212+
- database
213+
- host
214+
type: object
215+
postgresql:
216+
description: Connection settings for a [PostgreSQL](https://www.postgresql.org/) database.
217+
properties:
218+
credentialsSecretName:
219+
description: |-
220+
Name of a Secret containing the `username` and `password` keys used to authenticate
221+
against the PostgreSQL server.
222+
type: string
223+
database:
224+
description: Name of the database (schema) to connect to.
225+
type: string
226+
host:
227+
description: Hostname or IP address of the PostgreSQL server.
228+
type: string
229+
parameters:
230+
additionalProperties:
231+
type: string
232+
default: {}
233+
description: |-
234+
Additional map of JDBC connection parameters to append to the connection URL. The given
235+
`HashMap<String, String>` will be converted to query parameters in the form of
236+
`?param1=value1&param2=value2`.
237+
type: object
238+
port:
239+
default: 5432
240+
description: Port the PostgreSQL server is listening on. Defaults to `5432`.
241+
format: uint16
242+
maximum: 65535.0
243+
minimum: 0.0
244+
type: integer
245+
required:
246+
- credentialsSecretName
247+
- database
248+
- host
249+
type: object
250+
redis:
251+
description: |-
252+
Connection settings for a [Redis](https://redis.io/) instance.
253+
254+
Redis is commonly used as a Celery message broker or result backend (e.g. for Apache Airflow).
255+
properties:
256+
credentialsSecretName:
257+
description: |-
258+
Name of a Secret containing the `username` and `password` keys used to authenticate
259+
against the Redis server.
260+
type: string
261+
databaseId:
262+
default: 0
263+
description: |-
264+
Numeric index of the Redis logical database to use. Defaults to `0`.
265+
266+
Redis supports multiple logical databases within a single instance, identified by an
267+
integer index. Database `0` is the default.
268+
format: uint16
269+
maximum: 65535.0
270+
minimum: 0.0
271+
type: integer
272+
host:
273+
description: Hostname or IP address of the Redis server.
274+
type: string
275+
port:
276+
default: 6379
277+
description: Port the Redis server is listening on. Defaults to `6379`.
278+
format: uint16
279+
maximum: 65535.0
280+
minimum: 0.0
281+
type: integer
282+
required:
283+
- credentialsSecretName
284+
- host
285+
type: object
286+
type: object
85287
domainName:
86288
description: A validated domain name type conforming to RFC 1123, so e.g. not an IP address
87289
type: string
@@ -2188,6 +2390,7 @@ spec:
21882390
required:
21892391
- clientAuthenticationDetails
21902392
- clusterOperation
2393+
- databaseConnection
21912394
- domainName
21922395
- gitSync
21932396
- hostName

crates/stackable-operator/src/builder/pod/container.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ impl ContainerBuilder {
175175
self
176176
}
177177

178-
pub fn add_env_vars(&mut self, env_vars: Vec<EnvVar>) -> &mut Self {
178+
pub fn add_env_vars<I>(&mut self, env_vars: I) -> &mut Self
179+
where
180+
I: IntoIterator<Item = EnvVar>,
181+
{
179182
self.env.get_or_insert_with(Vec::new).extend(env_vars);
180183
self
181184
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, SecretKeySelector};
2+
3+
pub fn env_var_with_value_from_secret(
4+
env_var_name: impl Into<String>,
5+
secret_name: impl Into<String>,
6+
secret_key: impl Into<String>,
7+
) -> EnvVar {
8+
EnvVar {
9+
name: env_var_name.into(),
10+
value_from: Some(EnvVarSource {
11+
secret_key_ref: Some(SecretKeySelector {
12+
name: secret_name.into(),
13+
key: secret_key.into(),
14+
..Default::default()
15+
}),
16+
..Default::default()
17+
}),
18+
..Default::default()
19+
}
20+
}

crates/stackable-operator/src/builder/pod/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::{
2929
};
3030

3131
pub mod container;
32+
pub mod env;
3233
pub mod probe;
3334
pub mod resources;
3435
pub mod security;

crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ impl GitSyncResources {
197197
one_time,
198198
container_log_config,
199199
)])
200-
.add_env_vars(env_vars.into())
200+
.add_env_vars(env_vars.iter().cloned())
201201
.add_volume_mounts(volume_mounts.to_vec())
202202
.context(AddVolumeMountSnafu)?
203203
.resources(

crates/stackable-operator/src/crd/git_sync/v1alpha2_impl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ impl GitSyncResources {
314314
container_log_config,
315315
ca_cert_path,
316316
)])
317-
.add_env_vars(env_vars.into())
317+
.add_env_vars(env_vars.iter().cloned())
318318
.add_volume_mounts(volume_mounts.to_vec())
319319
.context(AddVolumeMountSnafu)?
320320
.resources(
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::path::PathBuf;
2+
3+
use schemars::JsonSchema;
4+
use serde::{Deserialize, Serialize};
5+
use snafu::{OptionExt, ResultExt, Snafu};
6+
7+
use crate::{
8+
database_connections::{
9+
TemplatingMechanism,
10+
drivers::jdbc::{JdbcDatabaseConnection, JdbcDatabaseConnectionDetails},
11+
},
12+
utils::OptionExt as _,
13+
};
14+
15+
/// Sadly the Derby driver class name is a bit complicated, e.g. for HMS up to 4.1.x we used
16+
/// `org.apache.derby.jdbc.EmbeddedDriver`, for HMS 4.2.x we used
17+
/// `org.apache.derby.iapi.jdbc.AutoloadedDriver`.
18+
pub const DERBY_JDBC_DRIVER_CLASS: &str = "org.apache.derby.jdbc.EmbeddedDriver";
19+
20+
#[derive(Debug, Snafu)]
21+
pub enum Error {
22+
#[snafu(display("failed to parse connection URL"))]
23+
ParseConnectionUrl { source: url::ParseError },
24+
25+
#[snafu(display("invalid derby database location, likely as it contains non-utf8 characters"))]
26+
NonUtf8Location { location: PathBuf },
27+
}
28+
29+
/// Connection settings for an embedded [Apache Derby](https://db.apache.org/derby/) database.
30+
///
31+
/// Derby is an embedded, file-based Java database engine that requires no separate server process.
32+
/// It is typically used for development, testing, or as a lightweight metastore backend (e.g. for
33+
/// Apache Hive).
34+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
35+
#[serde(rename_all = "camelCase")]
36+
pub struct DerbyConnection {
37+
/// Path on the filesystem where Derby stores its database files.
38+
///
39+
/// If not specified, defaults to `/tmp/derby/{unique_database_name}/derby.db`.
40+
/// The `{unique_database_name}` part is automatically handled by the operator and is added to
41+
/// prevent clashing database files. The `create=true` flag is always appended to the JDBC URL,
42+
/// so the database is created automatically if it does not yet exist at this location.
43+
pub location: Option<PathBuf>,
44+
}
45+
46+
impl JdbcDatabaseConnection for DerbyConnection {
47+
fn jdbc_connection_details_with_templating(
48+
&self,
49+
unique_database_name: &str,
50+
_templating_mechanism: &TemplatingMechanism,
51+
) -> Result<JdbcDatabaseConnectionDetails, crate::database_connections::Error> {
52+
let location = self.location.as_ref_or_else(|| {
53+
PathBuf::from(format!("/tmp/derby/{unique_database_name}/derby.db"))
54+
});
55+
let location = location.to_str().with_context(|| NonUtf8LocationSnafu {
56+
location: location.to_path_buf(),
57+
})?;
58+
let connection_url = format!("jdbc:derby:{location};create=true",);
59+
let connection_url = connection_url.parse().context(ParseConnectionUrlSnafu)?;
60+
61+
Ok(JdbcDatabaseConnectionDetails {
62+
driver: DERBY_JDBC_DRIVER_CLASS.to_owned(),
63+
connection_url,
64+
username_env: None,
65+
password_env: None,
66+
})
67+
}
68+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod derby;
2+
pub mod mysql;
3+
pub mod postgresql;
4+
pub mod redis;

0 commit comments

Comments
 (0)