Skip to content

Commit d4ed55d

Browse files
sbernauerTechassi
andauthored
feat!: Add mandatory provison_parts argument to SecretOperatorVolumeSourceBuilder::new (#1165)
* feat!: Add mandatory provison_parts argument to SecretOperatorVolumeSourceBuilder::new * Full -> All * fix tests * Rename all -> public-private * changelog * Move enum * Derive Copy * Improve changelog * chore: Slightly improve annotation insertion * chore: Adjust doc comment, add TODO dev comment * test: Rename unit test to reflect what is actually tested * fix: Bump rustls-webpki to 0.103.10 to negate RUSTSEC-2026-0049 * Fix compilation --------- Co-authored-by: Techassi <git@techassi.dev>
1 parent fbebe74 commit d4ed55d

File tree

9 files changed

+115
-22
lines changed

9 files changed

+115
-22
lines changed

crates/stackable-operator/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,23 @@ All notable changes to this project will be documented in this file.
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]).
1313

14+
### Changed
15+
16+
- BREAKING: Add mandatory `provision_parts` argument to `SecretOperatorVolumeSourceBuilder::new` ([#1165]).
17+
It now forces the caller to make an explicit choice if the public parts are sufficient or if private
18+
(e.g. a certificate for the Pod) parts are needed as well. This is done to avoid accidentally requesting
19+
too much parts. For details see [this issue](https://github.com/stackabletech/issues/issues/547).
20+
21+
Additionally, `SecretClassVolume::to_volume` and `SecretClassVolume::to_ephemeral_volume_source`
22+
also take the same new argument.
23+
1424
### Removed
1525

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

1929
[#1154]: https://github.com/stackabletech/operator-rs/pull/1154
30+
[#1165]: https://github.com/stackabletech/operator-rs/pull/1165
2031
[#1178]: https://github.com/stackabletech/operator-rs/pull/1178
2132
[#1182]: https://github.com/stackabletech/operator-rs/pull/1182
2233

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/secret_class.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ impl SecretClassVolume {
3838

3939
pub fn to_ephemeral_volume_source(
4040
&self,
41+
provision_parts: SecretClassVolumeProvisionParts,
4142
) -> Result<EphemeralVolumeSource, SecretClassVolumeError> {
4243
let mut secret_operator_volume_builder =
43-
SecretOperatorVolumeSourceBuilder::new(&self.secret_class);
44+
SecretOperatorVolumeSourceBuilder::new(&self.secret_class, provision_parts);
4445

4546
if let Some(scope) = &self.scope {
4647
if scope.pod {
@@ -62,8 +63,12 @@ impl SecretClassVolume {
6263
.context(SecretOperatorVolumeSnafu)
6364
}
6465

65-
pub fn to_volume(&self, volume_name: &str) -> Result<Volume, SecretClassVolumeError> {
66-
let ephemeral = self.to_ephemeral_volume_source()?;
66+
pub fn to_volume(
67+
&self,
68+
volume_name: &str,
69+
provision_parts: SecretClassVolumeProvisionParts,
70+
) -> Result<Volume, SecretClassVolumeError> {
71+
let ephemeral = self.to_ephemeral_volume_source(provision_parts)?;
6772
Ok(VolumeBuilder::new(volume_name).ephemeral(ephemeral).build())
6873
}
6974
}
@@ -94,14 +99,33 @@ pub struct SecretClassVolumeScope {
9499
pub listener_volumes: Vec<String>,
95100
}
96101

102+
/// What parts of secret material should be provisioned into the requested volume.
103+
//
104+
// There intentionally isn't a global [`Default`] impl, as it is secret-operator's concern what it
105+
// chooses as a default.
106+
// TODO (@Techassi): This to me is a HUGE indicator this lives in the wrong place. All these secret
107+
// volume builders/helpers should be defined as part of a secret-operator library to be as close as
108+
// possible to secret-operator, which is the authoritative source of truth for all of this.
109+
#[derive(Copy, Clone, Debug, PartialEq, Eq, strum::AsRefStr)]
110+
#[strum(serialize_all = "kebab-case")]
111+
pub enum SecretClassVolumeProvisionParts {
112+
/// Only provision public parts, such as the CA certificate (either as PEM or truststore) or
113+
/// `krb5.conf`.
114+
Public,
115+
116+
/// Provision all parts, which includes all [`Public`](Self::Public) ones as well as additional
117+
/// private parts, such as a TLS cert + private key, a keystore or a keytab.
118+
PublicPrivate,
119+
}
120+
97121
#[cfg(test)]
98122
mod tests {
99123
use std::collections::BTreeMap;
100124

101125
use super::*;
102126

103127
#[test]
104-
fn volume_to_csi_volume_source() {
128+
fn volume_to_ephemeral_volume_source() {
105129
let secret_class_volume_source = SecretClassVolume {
106130
secret_class: "myclass".to_string(), // pragma: allowlist secret
107131
scope: Some(SecretClassVolumeScope {
@@ -111,7 +135,8 @@ mod tests {
111135
listener_volumes: vec!["mylistener".to_string()],
112136
}),
113137
}
114-
.to_ephemeral_volume_source()
138+
// Let's assume we need some form of private data (e.g. a certificate or S3 credentials)
139+
.to_ephemeral_volume_source(SecretClassVolumeProvisionParts::PublicPrivate)
115140
.unwrap();
116141

117142
let expected_volume_attributes = BTreeMap::from([
@@ -123,6 +148,10 @@ mod tests {
123148
"secrets.stackable.tech/scope".to_string(),
124149
"pod,service=myservice,listener-volume=mylistener".to_string(),
125150
),
151+
(
152+
"secrets.stackable.tech/provision-parts".to_string(),
153+
"public-private".to_string(),
154+
),
126155
]);
127156

128157
assert_eq!(

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use crate::{
88
self,
99
pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder},
1010
},
11-
commons::secret_class::{SecretClassVolume, SecretClassVolumeError},
11+
commons::secret_class::{
12+
SecretClassVolume, SecretClassVolumeError, SecretClassVolumeProvisionParts,
13+
},
1214
constants::secret::SECRET_BASE_PATH,
1315
};
1416

@@ -107,7 +109,8 @@ impl TlsClientDetails {
107109
let volume_name = format!("{secret_class}-ca-cert");
108110
let secret_class_volume = SecretClassVolume::new(secret_class.clone(), None);
109111
let volume = secret_class_volume
110-
.to_volume(&volume_name)
112+
// We only need the public CA cert
113+
.to_volume(&volume_name, SecretClassVolumeProvisionParts::Public)
111114
.context(SecretClassVolumeSnafu)?;
112115

113116
volumes.push(volume);

crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use crate::{
77
self,
88
pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder},
99
},
10-
commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError},
10+
commons::{
11+
secret_class::{SecretClassVolumeError, SecretClassVolumeProvisionParts},
12+
tls_verification::TlsClientDetailsError,
13+
},
1114
constants::secret::SECRET_BASE_PATH,
1215
crd::authentication::ldap::v1alpha1::{AuthenticationProvider, FieldNames},
1316
};
@@ -94,7 +97,8 @@ impl AuthenticationProvider {
9497
let secret_class = &bind_credentials.secret_class;
9598
let volume_name = format!("{secret_class}-bind-credentials");
9699
let volume = bind_credentials
97-
.to_volume(&volume_name)
100+
// We need the private LDAP bind credentials
101+
.to_volume(&volume_name, SecretClassVolumeProvisionParts::PublicPrivate)
98102
.context(BindCredentialsSnafu)?;
99103

100104
volumes.push(volume);
@@ -234,7 +238,10 @@ mod tests {
234238
secret_class: "ldap-ca-cert".to_string(),
235239
scope: None,
236240
}
237-
.to_volume("ldap-ca-cert-ca-cert")
241+
.to_volume(
242+
"ldap-ca-cert-ca-cert",
243+
SecretClassVolumeProvisionParts::Public
244+
)
238245
.unwrap()
239246
]
240247
);
@@ -263,13 +270,19 @@ mod tests {
263270
secret_class: "openldap-bind-credentials".to_string(),
264271
scope: None,
265272
}
266-
.to_volume("openldap-bind-credentials-bind-credentials")
273+
.to_volume(
274+
"openldap-bind-credentials-bind-credentials",
275+
SecretClassVolumeProvisionParts::PublicPrivate
276+
)
267277
.unwrap(),
268278
SecretClassVolume {
269279
secret_class: "ldap-ca-cert".to_string(),
270280
scope: None,
271281
}
272-
.to_volume("ldap-ca-cert-ca-cert")
282+
.to_volume(
283+
"ldap-ca-cert-ca-cert",
284+
SecretClassVolumeProvisionParts::Public
285+
)
273286
.unwrap()
274287
]
275288
);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
commons::{
1616
self,
1717
product_image_selection::ResolvedProductImage,
18-
secret_class::SecretClassVolume,
18+
secret_class::{SecretClassVolume, SecretClassVolumeProvisionParts},
1919
tls_verification::{CaCert, TlsServerVerification, TlsVerification},
2020
},
2121
crd::git_sync::v1alpha2::{Credentials, GitSync},
@@ -276,7 +276,8 @@ impl GitSyncResources {
276276
let secret_class_volume = SecretClassVolume::new(secret_class.clone(), None);
277277
let volume_name = format!("{CA_CERT_VOLUME_NAME_PREFIX}-{i}");
278278
let ca_cert_secret_volume = secret_class_volume
279-
.to_volume(&volume_name)
279+
// We only need the public CA cert
280+
.to_volume(&volume_name, SecretClassVolumeProvisionParts::Public)
280281
.context(SecretClassVolumeSnafu)?;
281282
resources.git_ca_cert_volumes.push(ca_cert_secret_volume);
282283
}
@@ -1390,6 +1391,7 @@ name: content-from-git-0
13901391
metadata:
13911392
annotations:
13921393
secrets.stackable.tech/class: git-tls-ca
1394+
secrets.stackable.tech/provision-parts: public
13931395
spec:
13941396
accessModes:
13951397
- ReadWriteOnce

crates/stackable-operator/src/crd/s3/connection/mod.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,16 @@ mod tests {
174174
.unwrap()
175175
.annotations
176176
.unwrap(),
177-
&BTreeMap::from([(
178-
"secrets.stackable.tech/class".to_string(),
179-
"ionos-s3-credentials".to_string()
180-
)]),
177+
&BTreeMap::from([
178+
(
179+
"secrets.stackable.tech/class".to_string(),
180+
"ionos-s3-credentials".to_string()
181+
),
182+
(
183+
"secrets.stackable.tech/provision-parts".to_string(),
184+
"public-private".to_string()
185+
)
186+
]),
181187
);
182188

183189
assert_eq!(mount.name, volume.name);

crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use url::Url;
55
use crate::{
66
builder::pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder},
77
client::Client,
8-
commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError},
8+
commons::{
9+
secret_class::{SecretClassVolumeError, SecretClassVolumeProvisionParts},
10+
tls_verification::TlsClientDetailsError,
11+
},
912
constants::secret::SECRET_BASE_PATH,
1013
crd::s3::{
1114
connection::ResolvedConnection,
@@ -110,7 +113,8 @@ impl ConnectionSpec {
110113

111114
volumes.push(
112115
credentials
113-
.to_volume(&volume_name)
116+
// We need the private S3 credentials
117+
.to_volume(&volume_name, SecretClassVolumeProvisionParts::PublicPrivate)
114118
.context(AddS3CredentialVolumesSnafu)?,
115119
);
116120
mounts.push(

crates/stackable-operator/src/kvp/annotation/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use delegate::delegate;
1919

2020
use crate::{
2121
builder::pod::volume::SecretOperatorVolumeScope,
22+
commons::secret_class::SecretClassVolumeProvisionParts,
2223
iter::TryFromIterator,
2324
kvp::{Key, KeyValuePair, KeyValuePairError, KeyValuePairs, KeyValuePairsError},
2425
};
@@ -80,6 +81,15 @@ impl Annotation {
8081
self.0
8182
}
8283

84+
/// Constructs a `secrets.stackable.tech/provision-parts` annotation.
85+
pub fn secret_provision_parts(
86+
provision_parts: &SecretClassVolumeProvisionParts,
87+
) -> Result<Self, AnnotationError> {
88+
let kvp =
89+
KeyValuePair::try_from(("secrets.stackable.tech/provision-parts", provision_parts))?;
90+
Ok(Self(kvp))
91+
}
92+
8393
/// Constructs a `secrets.stackable.tech/class` annotation.
8494
pub fn secret_class(secret_class: &str) -> Result<Self, AnnotationError> {
8595
let kvp = KeyValuePair::try_from(("secrets.stackable.tech/class", secret_class))?;

0 commit comments

Comments
 (0)