Skip to content

Commit efe534f

Browse files
authored
feat: Add helper function to create random Secrets (#1187)
* Add create_random_secret_if_not_exists helper function * changelog
1 parent 930396f commit efe534f

File tree

6 files changed

+146
-0
lines changed

6 files changed

+146
-0
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
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

crates/stackable-operator/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ All notable changes to this project will be documented in this file.
1010
- Add support for specifying a `clientAuthenticationMethod` for OIDC ([#1178]).
1111
This was originally done in [#1158] and had been reverted in [#1170].
1212
- Implement `Deref` for `kvp::Key` to be more ergonomic to use ([#1182]).
13+
- Add `create_random_secret_if_not_exists` function, which create a random Secret in case it doesn't already exist.
14+
It notably also fixes a bug we had in trino and airflow-operator, where we created immutable Secrets,
15+
which lead to problems ([#1187]).
1316

1417
### Changed
1518

@@ -30,6 +33,7 @@ All notable changes to this project will be documented in this file.
3033
[#1165]: https://github.com/stackabletech/operator-rs/pull/1165
3134
[#1178]: https://github.com/stackabletech/operator-rs/pull/1178
3235
[#1182]: https://github.com/stackabletech/operator-rs/pull/1182
36+
[#1187]: https://github.com/stackabletech/operator-rs/pull/1187
3337

3438
## [0.108.0] - 2026-03-10
3539

crates/stackable-operator/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/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)