Skip to content

Commit 6597b32

Browse files
authored
Merge branch 'main' into feat/config-overrides-json
2 parents b19c190 + a4a204b commit 6597b32

34 files changed

Lines changed: 2014 additions & 456 deletions

File tree

Cargo.lock

Lines changed: 307 additions & 380 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ product-config = { git = "https://github.com/stackabletech/product-config.git",
1414
arc-swap = "1.7.0"
1515
async-trait = "0.1.89"
1616
axum = { version = "0.8.1", features = ["http2"] }
17+
base64 = "0.22"
1718
clap = { version = "4.5.17", features = ["derive", "cargo", "env"] }
1819
const_format = "0.2.33"
1920
# Cannot be updated until x509-cert uses a newer version
@@ -68,7 +69,7 @@ serde_json = "1.0.128"
6869
serde_yaml = "0.9.34" # This is the last available version, see https://github.com/dtolnay/serde-yaml/releases/tag/0.9.34 for details
6970
sha2 = { version = "0.10.8", features = ["oid"] }
7071
signature = "2.2.0"
71-
snafu = "0.8.4"
72+
snafu = "0.9.0"
7273
stackable-operator-derive = { path = "stackable-operator-derive" }
7374
strum = { version = "0.28.0", features = ["derive"] }
7475
syn = "2.0.77"

crates/stackable-operator/CHANGELOG.md

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

55
## [Unreleased]
66

7+
## [0.109.0] - 2026-04-07
8+
79
### Added
810

11+
- Git sync: add support for CAs ([#1154]).
912
- Add support for specifying a `clientAuthenticationMethod` for OIDC ([#1178]).
1013
This was originally done in [#1158] and had been reverted in [#1170].
14+
- Implement `Deref` for `kvp::Key` to be more ergonomic to use ([#1182]).
15+
- Add `create_random_secret_if_not_exists` function, which create a random Secret in case it doesn't already exist.
16+
It notably also fixes a bug we had in trino and airflow-operator, where we created immutable Secrets,
17+
which lead to problems ([#1187]).
18+
19+
### Changed
20+
21+
- Bump stackable-versioned to `0.9.0`, refer to its [changelog](../stackable-versioned/CHANGELOG.md) ([#1189]).
22+
- Bump stackable-webhook to `0.9.1`, refer to its [changelog](../stackable-webhook/CHANGELOG.md) ([#1189]).
23+
- BREAKING: Add mandatory `provision_parts` argument to `SecretOperatorVolumeSourceBuilder::new` ([#1165]).
24+
It now forces the caller to make an explicit choice if the public parts are sufficient or if private
25+
(e.g. a certificate for the Pod) parts are needed as well. This is done to avoid accidentally requesting
26+
too much parts. For details see [this issue](https://github.com/stackabletech/issues/issues/547).
27+
28+
Additionally, `SecretClassVolume::to_volume` and `SecretClassVolume::to_ephemeral_volume_source`
29+
also take the same new argument.
30+
31+
### Removed
32+
33+
- BREAKING: Remove unused `add_prefix`, `try_add_prefix`, `set_name`, and `try_set_name` associated
34+
functions from `kvp::Key` to disallow mutable access to inner values ([#1182]).
1135

36+
[#1154]: https://github.com/stackabletech/operator-rs/pull/1154
37+
[#1165]: https://github.com/stackabletech/operator-rs/pull/1165
1238
[#1178]: https://github.com/stackabletech/operator-rs/pull/1178
39+
[#1182]: https://github.com/stackabletech/operator-rs/pull/1182
40+
[#1187]: https://github.com/stackabletech/operator-rs/pull/1187
41+
[#1189]: https://github.com/stackabletech/operator-rs/pull/1189
1342

1443
## [0.108.0] - 2026-03-10
1544

crates/stackable-operator/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "stackable-operator"
33
description = "Stackable Operator Framework"
4-
version = "0.108.0"
4+
version = "0.109.0"
55
authors.workspace = true
66
license.workspace = true
77
edition.workspace = true
@@ -25,6 +25,7 @@ stackable-telemetry = { path = "../stackable-telemetry", features = ["clap"] }
2525
stackable-versioned = { path = "../stackable-versioned", optional = true }
2626
stackable-webhook = { path = "../stackable-webhook", optional = true }
2727

28+
base64.workspace = true
2829
clap.workspace = true
2930
const_format.workspace = true
3031
delegate.workspace = true
@@ -39,6 +40,7 @@ json-patch.workspace = true
3940
k8s-openapi.workspace = true
4041
kube.workspace = true
4142
product-config.workspace = true
43+
rand.workspace = true
4244
regex.workspace = true
4345
schemars.workspace = true
4446
semver.workspace = true

crates/stackable-operator/crds/DummyCluster.yaml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,56 @@ spec:
205205
description: 'The git repository URL that will be cloned, for example: `https://github.com/stackabletech/airflow-operator` or `ssh://git@github.com:stackable-airflow/dags.git`.'
206206
format: uri
207207
type: string
208+
tls:
209+
default:
210+
verification:
211+
server:
212+
caCert:
213+
webPki: {}
214+
description: Configure a TLS connection. If not specified it will default to webPki validation.
215+
nullable: true
216+
properties:
217+
verification:
218+
description: The verification method used to verify the certificates of the server and/or the client.
219+
oneOf:
220+
- required:
221+
- none
222+
- required:
223+
- server
224+
properties:
225+
none:
226+
description: Use TLS but don't verify certificates.
227+
type: object
228+
server:
229+
description: Use TLS and a CA certificate to verify the server.
230+
properties:
231+
caCert:
232+
description: CA cert to verify the server.
233+
oneOf:
234+
- required:
235+
- webPki
236+
- required:
237+
- secretClass
238+
properties:
239+
secretClass:
240+
description: |-
241+
Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate.
242+
Note that a SecretClass does not need to have a key but can also work with just a CA certificate,
243+
so if you got provided with a CA cert but don't have access to the key you can still use this method.
244+
type: string
245+
webPki:
246+
description: |-
247+
Use TLS and the CA certificates trusted by the common web browsers to verify the server.
248+
This can be useful when you e.g. use public AWS S3 or other public available services.
249+
type: object
250+
type: object
251+
required:
252+
- caCert
253+
type: object
254+
type: object
255+
required:
256+
- verification
257+
type: object
208258
wait:
209259
default: 20s
210260
description: |-

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use tracing::warn;
1414

1515
use crate::{
1616
builder::meta::ObjectMetaBuilder,
17+
commons::secret_class::SecretClassVolumeProvisionParts,
1718
kvp::{Annotation, AnnotationError, Annotations, LabelError, Labels},
1819
};
1920

@@ -281,17 +282,29 @@ pub struct SecretOperatorVolumeSourceBuilder {
281282
kerberos_service_names: Vec<String>,
282283
tls_pkcs12_password: Option<String>,
283284
auto_tls_cert_lifetime: Option<Duration>,
285+
provision_parts: SecretClassVolumeProvisionParts,
284286
}
285287

286288
impl SecretOperatorVolumeSourceBuilder {
287-
pub fn new(secret_class: impl Into<String>) -> Self {
289+
/// Creates a builder for a secret-operator volume that uses the specified SecretClass to
290+
/// request the specified [`SecretClassVolumeProvisionParts`].
291+
///
292+
/// This function forces the caller to make an explicit choice if the public parts are
293+
/// sufficient or if private (e.g. a certificate for the Pod) parts are needed as well.
294+
/// This is done to avoid accidentally requesting too much parts. For details see
295+
/// [this issue](https://github.com/stackabletech/issues/issues/547).
296+
pub fn new(
297+
secret_class: impl Into<String>,
298+
provision_parts: SecretClassVolumeProvisionParts,
299+
) -> Self {
288300
Self {
289301
secret_class: secret_class.into(),
290302
scopes: Vec::new(),
291303
format: None,
292304
kerberos_service_names: Vec::new(),
293305
tls_pkcs12_password: None,
294306
auto_tls_cert_lifetime: None,
307+
provision_parts,
295308
}
296309
}
297310

@@ -340,8 +353,10 @@ impl SecretOperatorVolumeSourceBuilder {
340353
pub fn build(&self) -> Result<EphemeralVolumeSource, SecretOperatorVolumeSourceBuilderError> {
341354
let mut annotations = Annotations::new();
342355

356+
#[rustfmt::skip]
343357
annotations
344-
.insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?);
358+
.insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?)
359+
.insert(Annotation::secret_provision_parts(&self.provision_parts).context(ParseAnnotationSnafu)?);
345360

346361
if !self.scopes.is_empty() {
347362
annotations

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod networking;
77
pub mod opa;
88
pub mod pdb;
99
pub mod product_image_selection;
10+
pub mod random_secret_creation;
1011
pub mod rbac;
1112
pub mod resources;
1213
pub mod secret_class;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use std::collections::BTreeMap;
2+
3+
use base64::Engine;
4+
use k8s_openapi::api::core::v1::Secret;
5+
use kube::{Api, Resource, ResourceExt, api::DeleteParams};
6+
use rand::{RngCore, SeedableRng, rngs::StdRng};
7+
use snafu::{OptionExt, ResultExt, Snafu};
8+
9+
use crate::{builder::meta::ObjectMetaBuilder, client::Client};
10+
11+
#[derive(Snafu, Debug)]
12+
pub enum Error {
13+
#[snafu(display("object defines no namespace"))]
14+
ObjectHasNoNamespace,
15+
16+
#[snafu(display("failed to retrieve random secret"))]
17+
RetrieveRandomSecret { source: crate::client::Error },
18+
19+
#[snafu(display("failed to create random secret"))]
20+
CreateRandomSecret { source: crate::client::Error },
21+
22+
#[snafu(display("failed to delete random secret"))]
23+
DeleteRandomSecret { source: kube::Error },
24+
25+
#[snafu(display("object is missing metadata to build owner reference"))]
26+
ObjectMissingMetadataForOwnerRef { source: crate::builder::meta::Error },
27+
}
28+
29+
/// This function creates a random Secret if it doesn't already exist.
30+
///
31+
/// As this function generates random Secret contents, if the Secret already exists, it will *not*
32+
/// be patched, as otherwise we would generate new Secret contents on every reconcile. Which would
33+
/// in turn cause Pods restarts, which would cause reconciles ;)
34+
///
35+
/// However, there is one special handling needed:
36+
///
37+
/// We can't mark Secrets as immutable, as this caused problems, see https://github.com/stackabletech/issues/issues/843.
38+
/// As Secrets have been created as immutable up to SDP release 26.3.0, we need to delete the, to be
39+
/// able to re-create them as mutable. This function detects old (immutable) Secrets and re-creates
40+
/// them as mutable. The contents of the Secret will be kept to prevent unnecessary Secret content
41+
/// changes.
42+
//
43+
// TODO: This can be removed in a future SDP release, likely 26.11, as all Secrets have been migrated.
44+
pub async fn create_random_secret_if_not_exists<R>(
45+
secret_name: &str,
46+
secret_key: &str,
47+
secret_size_bytes: usize,
48+
stacklet: &R,
49+
client: &Client,
50+
) -> Result<(), Error>
51+
where
52+
R: Resource<DynamicType = ()>,
53+
{
54+
let secret_namespace = stacklet.namespace().context(ObjectHasNoNamespaceSnafu)?;
55+
let existing_secret = client
56+
.get_opt::<Secret>(secret_name, &secret_namespace)
57+
.await
58+
.context(RetrieveRandomSecretSnafu)?;
59+
60+
match existing_secret {
61+
Some(
62+
existing_secret @ Secret {
63+
immutable: Some(true),
64+
..
65+
},
66+
) => {
67+
tracing::info!(
68+
k8s.secret.name = secret_name,
69+
k8s.secret.namespace = secret_namespace,
70+
"Old (immutable) random Secret detected, re-creating it to be able to make it mutable. The contents will stay the same."
71+
);
72+
Api::<Secret>::namespaced(client.as_kube_client(), &secret_namespace)
73+
.delete(secret_name, &DeleteParams::default())
74+
.await
75+
.context(DeleteRandomSecretSnafu)?;
76+
77+
let mut mutable_secret = existing_secret;
78+
mutable_secret.immutable = Some(false);
79+
// Prevent "ApiError: resourceVersion should not be set on objects to be created"
80+
mutable_secret.metadata.resource_version = None;
81+
82+
client
83+
.create(&mutable_secret)
84+
.await
85+
.context(CreateRandomSecretSnafu)?;
86+
87+
// Note: restart-controller will restart all Pods mounting this Secret, as it has
88+
// changed.
89+
}
90+
Some(_) => {
91+
tracing::debug!(
92+
k8s.secret.name = secret_name,
93+
k8s.secret.namespace = secret_namespace,
94+
"Existing (mutable) random Secret detected, nothing to do"
95+
);
96+
}
97+
None => {
98+
tracing::info!(
99+
k8s.secret.name = secret_name,
100+
k8s.secret.namespace = secret_namespace,
101+
"Random Secret missing, creating it"
102+
);
103+
let secret = Secret {
104+
metadata: ObjectMetaBuilder::new()
105+
.name(secret_name)
106+
.namespace_opt(stacklet.namespace())
107+
.ownerreference_from_resource(stacklet, None, Some(true))
108+
.context(ObjectMissingMetadataForOwnerRefSnafu)?
109+
.build(),
110+
string_data: Some(BTreeMap::from([(
111+
secret_key.to_string(),
112+
get_random_base64(secret_size_bytes),
113+
)])),
114+
..Secret::default()
115+
};
116+
client
117+
.create(&secret)
118+
.await
119+
.context(CreateRandomSecretSnafu)?;
120+
}
121+
}
122+
123+
Ok(())
124+
}
125+
126+
/// Generates a cryptographically secure base64 String with the specified size in bytes.
127+
fn get_random_base64(size_bytes: usize) -> String {
128+
// As we are using the OS rng, we are using `getrandom`, which should be cryptographically
129+
// secure
130+
let mut rng = StdRng::from_os_rng();
131+
132+
let mut bytes = vec![0u8; size_bytes];
133+
rng.fill_bytes(&mut bytes);
134+
135+
base64::engine::general_purpose::STANDARD.encode(bytes)
136+
}

0 commit comments

Comments
 (0)