Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
df6084b
WIP
sbernauer Feb 11, 2026
f8a0b62
postgres -> postgresql
sbernauer Feb 11, 2026
1dc73a1
Store username and password envs separately
sbernauer Feb 12, 2026
c581a5b
Better name modules
sbernauer Feb 12, 2026
32c0ec9
Remove leftover code
sbernauer Feb 12, 2026
b9a16a9
Switch to config-utils templating
sbernauer Feb 12, 2026
b51f227
Add TODO marker
sbernauer Feb 12, 2026
159faac
Add MySQL
sbernauer Feb 12, 2026
9bb1e95
Improve Derby JDBC driver
sbernauer Feb 12, 2026
c2799fa
empty commit
sbernauer Feb 12, 2026
69fbb2a
Auto-create Derby database
sbernauer Feb 12, 2026
675dcc8
Merge remote-tracking branch 'origin/main' into spike/generic-databases
sbernauer Mar 2, 2026
f457c20
Update module structure
sbernauer Mar 2, 2026
7c193c4
clippy
sbernauer Mar 2, 2026
9e31680
cargo fmt
sbernauer Mar 2, 2026
33ae519
Implement CeleryConnection for Postgresql
sbernauer Mar 2, 2026
cfa085d
Support specifying the templating mechanism
sbernauer Mar 2, 2026
8e05224
fix: Use $VAR instead of ${VAR} (for Airflow)
sbernauer Mar 3, 2026
49f8ba0
Revert "fix: Use $VAR instead of ${VAR} (for Airflow)"
sbernauer Mar 3, 2026
3e391ff
Add lots of docs
sbernauer Mar 4, 2026
a21b8bd
Merge branch 'main' into spike/generic-databases
sbernauer Mar 4, 2026
51cfff6
Merge branch 'main' into spike/generic-databases
sbernauer Mar 19, 2026
43730e2
Improve docs
sbernauer Mar 19, 2026
75efe83
Merge branch 'main' into spike/generic-databases
maltesander Mar 23, 2026
cb04eb0
Apply suggestions from code review
sbernauer Mar 30, 2026
9fb9d8d
refactor!: Take IntoIterator<Item = EnvVar> instead
sbernauer Mar 31, 2026
0f81de5
Remove too specific JDBC
sbernauer Mar 31, 2026
357a288
JDBC -> Jdbc and SQLAlchemy -> SqlAlchemy
sbernauer Mar 31, 2026
6242c31
Update crates/stackable-operator/src/databases/databases/derby.rs
sbernauer Mar 31, 2026
62c019d
fix compilation
sbernauer Mar 31, 2026
656346b
refactor: Let connection_parameters_as_url_query_parameters return Op…
sbernauer Mar 31, 2026
83907d7
Put "org.postgresql.Driver" behind POSTGRES_JDBC_DRIVER_CLASS constant
sbernauer Mar 31, 2026
31cc9f5
Merge remote-tracking branch 'origin/main' into spike/generic-databases
sbernauer Mar 31, 2026
c6bdce6
refactor: Switch derby location from String to PathBuf
sbernauer Mar 31, 2026
205d9a5
Clarify what add_to_container does
sbernauer Mar 31, 2026
a3d1e6e
Remove bullet points
sbernauer Mar 31, 2026
8618adf
Add docs on TemplatingMechanism
sbernauer Mar 31, 2026
d445d37
Rename "databases" module to "database_connections"
sbernauer Mar 31, 2026
655e4ff
Explain context(false) and improve error messages
sbernauer Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ All notable changes to this project will be documented in this file.
- Implement `Deref` for `kvp::Key` to be more ergonomic to use ([#1182]).
- Add support for specifying a `clientAuthenticationMethod` for OIDC ([#1178]).
This was originally done in [#1158] and had been reverted in [#1170].
- Add generic database connection mechanism ([#1163]).

### Changed

- BREAKING: Change signature of `ContainerBuilder::add_env_vars` from `Vec<EnvVar>` to `IntoIterator<Item = EnvVar>` ([#1163]).

### Removed

- BREAKING: Remove unused `add_prefix`, `try_add_prefix`, `set_name`, and `try_set_name` associated
functions from `kvp::Key` to disallow mutable access to inner values ([#1182]).

[#1163]: https://github.com/stackabletech/operator-rs/pull/1163
[#1178]: https://github.com/stackabletech/operator-rs/pull/1178
[#1182]: https://github.com/stackabletech/operator-rs/pull/1182

Expand Down
203 changes: 203 additions & 0 deletions crates/stackable-operator/crds/DummyCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,208 @@ spec:
and `stopped` will take no effect until `reconciliationPaused` is set to false or removed.
type: boolean
type: object
databaseConnection:
oneOf:
- required:
- postgresql
- required:
- mysql
- required:
- derby
- required:
- redis
- required:
- genericJdbc
- required:
- genericSqlAlchemy
- required:
- genericCelery
properties:
derby:
description: |-
Connection settings for an embedded [Apache Derby](https://db.apache.org/derby/) database.

Derby is an embedded, file-based Java database engine that requires no separate server process.
It is typically used for development, testing, or as a lightweight metastore backend (e.g. for
Apache Hive).
properties:
location:
description: |-
Path on the filesystem where Derby stores its database files.

If not specified, defaults to `/tmp/derby/{unique_database_name}/derby.db`.
The `{unique_database_name}` part is automatically handled by the operator and is added to
prevent clashing database files. The `create=true` flag is always appended to the JDBC URL,
so the database is created automatically if it does not yet exist at this location.
nullable: true
type: string
type: object
genericCelery:
description: |-
A generic Celery database connection for broker or result backend types not covered by a
dedicated variant.

Use this when you need a Celery-compatible connection that does not have a first-class
connection type. The complete connection URI is read from a Secret, giving the user full
control over the connection string.
properties:
uriSecret:
description: The name of the Secret that contains an `uri` key with the complete SQLAlchemy URI.
type: string
required:
- uriSecret
type: object
genericJdbc:
description: |-
A generic JDBC database connection for database types not covered by a dedicated variant.

Use this when you need to connect to a JDBC-compatible database that does not have a
first-class connection type. You are responsible for providing the correct driver class name
and a fully-formed JDBC URI as well as providing the needed classes on the Java classpath.
properties:
credentialsSecret:
description: |-
Name of a Secret containing the `username` and `password` keys used to authenticate
against the database.
type: string
driver:
description: |-
Fully-qualified Java class name of the JDBC driver, e.g. `org.postgresql.Driver` or
`com.mysql.jdbc.Driver`. The driver JAR must be provided by you on the classpath.
type: string
uri:
description: |-
The JDBC connection URI, e.g. `jdbc:postgresql://my-host:5432/mydb`. Credentials must
not be embedded in this URI; they are instead injected via environment variables sourced
from `credentials_secret`.
format: uri
type: string
required:
- credentialsSecret
- driver
- uri
type: object
genericSqlAlchemy:
description: |-
A generic SQLAlchemy database connection for database types not covered by a dedicated variant.

Use this when you need to connect to a SQLAlchemy-compatible database that does not have a
first-class connection type. The complete connection URI is read from a Secret, giving the user
full control over the connection string including any driver-specific options.
properties:
uriSecret:
description: The name of the Secret that contains an `uri` key with the complete SQLAlchemy URI.
type: string
required:
- uriSecret
type: object
mysql:
description: Connection settings for a [MySQL](https://www.mysql.com/) database.
properties:
credentialsSecret:
description: |-
Name of a Secret containing the `username` and `password` keys used to authenticate
against the MySQL server.
type: string
database:
description: Name of the database (schema) to connect to.
type: string
host:
description: Hostname or IP address of the MySQL server.
type: string
parameters:
additionalProperties:
type: string
default: {}
description: |-
Additional map of connection parameters to append to the connection URL. The given
`HashMap<String, String>` will be converted to query parameters in the form of
`?param1=value1&param2=value2`.
type: object
port:
default: 3306
description: Port the MySQL server is listening on. Defaults to `3306`.
format: uint16
maximum: 65535.0
minimum: 0.0
type: integer
required:
- credentialsSecret
- database
- host
type: object
postgresql:
description: Connection settings for a [PostgreSQL](https://www.postgresql.org/) database.
properties:
credentialsSecret:
description: |-
Name of a Secret containing the `username` and `password` keys used to authenticate
against the PostgreSQL server.
type: string
database:
description: Name of the database (schema) to connect to.
type: string
host:
description: Hostname or IP address of the PostgreSQL server.
type: string
parameters:
additionalProperties:
type: string
default: {}
description: |-
Additional map of JDBC connection parameters to append to the connection URL. The given
`HashMap<String, String>` will be converted to query parameters in the form of
`?param1=value1&param2=value2`.
type: object
port:
default: 5432
description: Port the PostgreSQL server is listening on. Defaults to `5432`.
format: uint16
maximum: 65535.0
minimum: 0.0
type: integer
required:
- credentialsSecret
- database
- host
type: object
redis:
description: |-
Connection settings for a [Redis](https://redis.io/) instance.

Redis is commonly used as a Celery message broker or result backend (e.g. for Apache Airflow).
properties:
credentialsSecret:
description: |-
Name of a Secret containing the `username` and `password` keys used to authenticate
against the Redis server.
type: string
databaseId:
default: 0
description: |-
Numeric index of the Redis logical database to use. Defaults to `0`.

Redis supports multiple logical databases within a single instance, identified by an
integer index. Database `0` is the default.
format: uint16
maximum: 65535.0
minimum: 0.0
type: integer
host:
description: Hostname or IP address of the Redis server.
type: string
port:
default: 6379
description: Port the Redis server is listening on. Defaults to `6379`.
format: uint16
maximum: 65535.0
minimum: 0.0
type: integer
required:
- credentialsSecret
- host
type: object
type: object
domainName:
description: A validated domain name type conforming to RFC 1123, so e.g. not an IP address
type: string
Expand Down Expand Up @@ -2138,6 +2340,7 @@ spec:
required:
- clientAuthenticationDetails
- clusterOperation
- databaseConnection
- domainName
- gitSync
- hostName
Expand Down
5 changes: 4 additions & 1 deletion crates/stackable-operator/src/builder/pod/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ impl ContainerBuilder {
self
}

pub fn add_env_vars(&mut self, env_vars: Vec<EnvVar>) -> &mut Self {
pub fn add_env_vars<I>(&mut self, env_vars: I) -> &mut Self
where
I: IntoIterator<Item = EnvVar>,
{
self.env.get_or_insert_with(Vec::new).extend(env_vars);
self
}
Expand Down
20 changes: 20 additions & 0 deletions crates/stackable-operator/src/builder/pod/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, SecretKeySelector};

pub fn env_var_from_secret(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I think this is not an appropriate name. We are not constructing an env var from a Kubernetes secret (which would need to be looked up), but we instead prepare the env var in such a way that it will source its value from a Secret (which the Kubernetes apiserver or the kubelet is responsible for).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT of env_var_with_value_from_secret? That matches the valueFrom attribute Kubernetes uses

env_var_name: impl Into<String>,
secret_name: impl Into<String>,
secret_key: impl Into<String>,
) -> EnvVar {
EnvVar {
name: env_var_name.into(),
value_from: Some(EnvVarSource {
secret_key_ref: Some(SecretKeySelector {
name: secret_name.into(),
key: secret_key.into(),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}
}
1 change: 1 addition & 0 deletions crates/stackable-operator/src/builder/pod/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::{
};

pub mod container;
pub mod env;
pub mod probe;
pub mod resources;
pub mod security;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ impl GitSyncResources {
one_time,
container_log_config,
)])
.add_env_vars(env_vars.into())
.add_env_vars(env_vars.iter().cloned())
.add_volume_mounts(volume_mounts.to_vec())
.context(AddVolumeMountSnafu)?
.resources(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ impl GitSyncResources {
one_time,
container_log_config,
)])
.add_env_vars(env_vars.into())
.add_env_vars(env_vars.iter().cloned())
.add_volume_mounts(volume_mounts.to_vec())
.context(AddVolumeMountSnafu)?
.resources(
Expand Down
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I find it awkward that we have src/databases/databases as a path. I think src/database/backends is a better fit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the list of derby, mysql, postgres and redis I think databases is the correct term. However, I renamed it to src/database_connections/databases in d445d37, which I think explains all parts well

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::path::PathBuf;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use snafu::{OptionExt, ResultExt, Snafu};

use crate::{
database_connections::{
TemplatingMechanism,
drivers::jdbc::{JdbcDatabaseConnection, JdbcDatabaseConnectionDetails},
},
utils::OptionExt as _,
};

#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("failed to parse connection URL"))]
ParseConnectionUrl { source: url::ParseError },

#[snafu(display("invalid derby database location, likely as it contains non-utf8 characters"))]
NonUtf8Location { location: PathBuf },
}

/// Connection settings for an embedded [Apache Derby](https://db.apache.org/derby/) database.
///
/// Derby is an embedded, file-based Java database engine that requires no separate server process.
/// It is typically used for development, testing, or as a lightweight metastore backend (e.g. for
/// Apache Hive).
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DerbyConnection {
/// Path on the filesystem where Derby stores its database files.
///
/// If not specified, defaults to `/tmp/derby/{unique_database_name}/derby.db`.
/// The `{unique_database_name}` part is automatically handled by the operator and is added to
/// prevent clashing database files. The `create=true` flag is always appended to the JDBC URL,
/// so the database is created automatically if it does not yet exist at this location.
pub location: Option<PathBuf>,
}

impl JdbcDatabaseConnection for DerbyConnection {
fn jdbc_connection_details_with_templating(
&self,
unique_database_name: &str,
_templating_mechanism: &TemplatingMechanism,
) -> Result<JdbcDatabaseConnectionDetails, crate::database_connections::Error> {
let location = self.location.as_ref_or_else(|| {
PathBuf::from(format!("/tmp/derby/{unique_database_name}/derby.db"))
});
let location = location.to_str().with_context(|| NonUtf8LocationSnafu {
location: location.to_path_buf(),
})?;
let connection_uri = format!("jdbc:derby:{location};create=true",);
let connection_uri = connection_uri.parse().context(ParseConnectionUrlSnafu)?;

Ok(JdbcDatabaseConnectionDetails {
// Sadly the Derby driver class name is a bit complicated, e.g. for HMS up to 4.1.x we used
// "org.apache.derby.jdbc.EmbeddedDriver",
// for HMS 4.2.x we used "org.apache.derby.iapi.jdbc.AutoloadedDriver".
driver: "org.apache.derby.jdbc.EmbeddedDriver".to_owned(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: These well-known driver names (for JDBC) should live in (associated) constants instead.

connection_uri,
username_env: None,
password_env: None,
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod derby;
pub mod mysql;
pub mod postgresql;
pub mod redis;
Loading
Loading