From a811dd51a5c7aebdc1366be1b54ca69da3a54d3d Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 2 Apr 2026 15:46:59 +0200 Subject: [PATCH 1/2] Add create_random_secret_if_not_exists helper function --- Cargo.lock | 2 + Cargo.toml | 1 + crates/stackable-operator/CHANGELOG.md | 4 + crates/stackable-operator/Cargo.toml | 2 + crates/stackable-operator/src/commons/mod.rs | 1 + .../src/commons/random_secret_creation.rs | 136 ++++++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 crates/stackable-operator/src/commons/random_secret_creation.rs diff --git a/Cargo.lock b/Cargo.lock index 9c4c68bfe..8ae49668f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2943,6 +2943,7 @@ dependencies = [ name = "stackable-operator" version = "0.108.0" dependencies = [ + "base64", "clap", "const_format", "delegate", @@ -2958,6 +2959,7 @@ dependencies = [ "k8s-openapi", "kube", "product-config", + "rand 0.9.2", "regex", "rstest", "schemars", diff --git a/Cargo.toml b/Cargo.toml index 42a36c9d3..8d3b68e5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ product-config = { git = "https://github.com/stackabletech/product-config.git", arc-swap = "1.7.0" async-trait = "0.1.89" axum = { version = "0.8.1", features = ["http2"] } +base64 = "0.22" clap = { version = "4.5.17", features = ["derive", "cargo", "env"] } const_format = "0.2.33" # Cannot be updated until x509-cert uses a newer version diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 743eb1638..d3119aa7f 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -10,6 +10,9 @@ All notable changes to this project will be documented in this file. - Add support for specifying a `clientAuthenticationMethod` for OIDC ([#1178]). This was originally done in [#1158] and had been reverted in [#1170]. - Implement `Deref` for `kvp::Key` to be more ergonomic to use ([#1182]). +- Add `create_random_secret_if_not_exists` function, which create a random Secret in case it doesn't already exist. + It notably also fixes a bug we had in trino and airflow-operator, where we created immutable Secrets, + which lead to problems ([#XXXX]). ### Changed @@ -30,6 +33,7 @@ All notable changes to this project will be documented in this file. [#1165]: https://github.com/stackabletech/operator-rs/pull/1165 [#1178]: https://github.com/stackabletech/operator-rs/pull/1178 [#1182]: https://github.com/stackabletech/operator-rs/pull/1182 +[#XXXX]: https://github.com/stackabletech/operator-rs/pull/XXXX ## [0.108.0] - 2026-03-10 diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index 597276fa4..f5ea660a6 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -25,6 +25,7 @@ stackable-telemetry = { path = "../stackable-telemetry", features = ["clap"] } stackable-versioned = { path = "../stackable-versioned", optional = true } stackable-webhook = { path = "../stackable-webhook", optional = true } +base64.workspace = true clap.workspace = true const_format.workspace = true delegate.workspace = true @@ -39,6 +40,7 @@ json-patch.workspace = true k8s-openapi.workspace = true kube.workspace = true product-config.workspace = true +rand.workspace = true regex.workspace = true schemars.workspace = true semver.workspace = true diff --git a/crates/stackable-operator/src/commons/mod.rs b/crates/stackable-operator/src/commons/mod.rs index 89e71fc2f..cc4ab30e3 100644 --- a/crates/stackable-operator/src/commons/mod.rs +++ b/crates/stackable-operator/src/commons/mod.rs @@ -7,6 +7,7 @@ pub mod networking; pub mod opa; pub mod pdb; pub mod product_image_selection; +pub mod random_secret_creation; pub mod rbac; pub mod resources; pub mod secret_class; diff --git a/crates/stackable-operator/src/commons/random_secret_creation.rs b/crates/stackable-operator/src/commons/random_secret_creation.rs new file mode 100644 index 000000000..ee6f8de95 --- /dev/null +++ b/crates/stackable-operator/src/commons/random_secret_creation.rs @@ -0,0 +1,136 @@ +use std::collections::BTreeMap; + +use base64::Engine; +use k8s_openapi::api::core::v1::Secret; +use kube::{Api, Resource, ResourceExt, api::DeleteParams}; +use rand::{RngCore, SeedableRng, rngs::StdRng}; +use snafu::{OptionExt, ResultExt, Snafu}; + +use crate::{builder::meta::ObjectMetaBuilder, client::Client}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("object defines no namespace"))] + ObjectHasNoNamespace, + + #[snafu(display("failed to retrieve random secret"))] + RetrieveRandomSecret { source: crate::client::Error }, + + #[snafu(display("failed to create random secret"))] + CreateRandomSecret { source: crate::client::Error }, + + #[snafu(display("failed to delete random secret"))] + DeleteRandomSecret { source: kube::Error }, + + #[snafu(display("object is missing metadata to build owner reference"))] + ObjectMissingMetadataForOwnerRef { source: crate::builder::meta::Error }, +} + +/// This function creates a random Secret if it doesn't already exist. +/// +/// As this function generates random Secret contents, if the Secret already exists, it will *not* +/// be patched, as otherwise we would generate new Secret contents on every reconcile. Which would +/// in turn cause Pods restarts, which would cause reconciles ;) +/// +/// However, there is one special handling needed: +/// +/// We can't mark Secrets as immutable, as this caused problems, see https://github.com/stackabletech/issues/issues/843. +/// As Secrets have been created as immutable up to SDP release 26.3.0, we need to delete the, to be +/// able to re-create them as mutable. This function detects old (immutable) Secrets and re-creates +/// them as mutable. The contents of the Secret will be kept to prevent unnecessary Secret content +/// changes. +// +// TODO: This can be removed in a future SDP release, likely 26.11, as all Secrets have been migrated. +pub async fn create_random_secret_if_not_exists( + secret_name: &str, + secret_key: &str, + secret_size_bytes: usize, + stacklet: &R, + client: &Client, +) -> Result<(), Error> +where + R: Resource, +{ + let secret_namespace = stacklet.namespace().context(ObjectHasNoNamespaceSnafu)?; + let existing_secret = client + .get_opt::(secret_name, &secret_namespace) + .await + .context(RetrieveRandomSecretSnafu)?; + + match existing_secret { + Some( + existing_secret @ Secret { + immutable: Some(true), + .. + }, + ) => { + tracing::info!( + k8s.secret.name = secret_name, + k8s.secret.namespace = secret_namespace, + "Old (immutable) random Secret detected, re-creating it to be able to make it mutable. The contents will stay the same." + ); + Api::::namespaced(client.as_kube_client(), &secret_namespace) + .delete(secret_name, &DeleteParams::default()) + .await + .context(DeleteRandomSecretSnafu)?; + + let mut mutable_secret = existing_secret; + mutable_secret.immutable = Some(false); + // Prevent "ApiError: resourceVersion should not be set on objects to be created" + mutable_secret.metadata.resource_version = None; + + client + .create(&mutable_secret) + .await + .context(CreateRandomSecretSnafu)?; + + // Note: restart-controller will restart all Pods mounting this Secret, as it has + // changed. + } + Some(_) => { + tracing::debug!( + k8s.secret.name = secret_name, + k8s.secret.namespace = secret_namespace, + "Existing (mutable) random Secret detected, nothing to do" + ); + } + None => { + tracing::info!( + k8s.secret.name = secret_name, + k8s.secret.namespace = secret_namespace, + "Random Secret missing, creating it" + ); + let secret = Secret { + metadata: ObjectMetaBuilder::new() + .name(secret_name) + .namespace_opt(stacklet.namespace()) + .ownerreference_from_resource(stacklet, None, Some(true)) + .context(ObjectMissingMetadataForOwnerRefSnafu)? + .build(), + string_data: Some(BTreeMap::from([( + secret_key.to_string(), + get_random_base64(secret_size_bytes), + )])), + ..Secret::default() + }; + client + .create(&secret) + .await + .context(CreateRandomSecretSnafu)?; + } + } + + Ok(()) +} + +/// Generates a cryptographically secure base64 String with the specified size in bytes. +fn get_random_base64(size_bytes: usize) -> String { + // As we are using the OS rng, we are using `getrandom`, which should be cryptographically + // secure + let mut rng = StdRng::from_os_rng(); + + let mut bytes = vec![0u8; size_bytes]; + rng.fill_bytes(&mut bytes); + + base64::engine::general_purpose::STANDARD.encode(bytes) +} From 25329503dbd5f5097a6e326aace5ae4c9fd12bcb Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 2 Apr 2026 15:51:38 +0200 Subject: [PATCH 2/2] changelog --- crates/stackable-operator/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index d3119aa7f..bd32b553e 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -12,7 +12,7 @@ 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 `create_random_secret_if_not_exists` function, which create a random Secret in case it doesn't already exist. It notably also fixes a bug we had in trino and airflow-operator, where we created immutable Secrets, - which lead to problems ([#XXXX]). + which lead to problems ([#1187]). ### Changed @@ -33,7 +33,7 @@ All notable changes to this project will be documented in this file. [#1165]: https://github.com/stackabletech/operator-rs/pull/1165 [#1178]: https://github.com/stackabletech/operator-rs/pull/1178 [#1182]: https://github.com/stackabletech/operator-rs/pull/1182 -[#XXXX]: https://github.com/stackabletech/operator-rs/pull/XXXX +[#1187]: https://github.com/stackabletech/operator-rs/pull/1187 ## [0.108.0] - 2026-03-10