Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ 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].

### Changed

- BREAKING: Add mandatory `provision_parts` argument to `SecretOperatorVolumeSourceBuilder::new` ([#1165]).
It now forces the caller to make an explicit choice if the public parts are sufficient or if private
(e.g. a certificate for the Pod) parts are needed as well. This is done to avoid accidentally requesting
too much parts. For details see [this issue](https://github.com/stackabletech/issues/issues/547).

Additionally, `SecretClassVolume::to_volume` and `SecretClassVolume::to_ephemeral_volume_source`
also take the same new argument.

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

## [0.108.0] - 2026-03-10
Expand Down
19 changes: 17 additions & 2 deletions crates/stackable-operator/src/builder/pod/volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use tracing::warn;

use crate::{
builder::meta::ObjectMetaBuilder,
commons::secret_class::SecretClassVolumeProvisionParts,
kvp::{Annotation, AnnotationError, Annotations, LabelError, Labels},
};

Expand Down Expand Up @@ -281,17 +282,29 @@ pub struct SecretOperatorVolumeSourceBuilder {
kerberos_service_names: Vec<String>,
tls_pkcs12_password: Option<String>,
auto_tls_cert_lifetime: Option<Duration>,
provision_parts: SecretClassVolumeProvisionParts,
}

impl SecretOperatorVolumeSourceBuilder {
pub fn new(secret_class: impl Into<String>) -> Self {
/// Creates a builder for a secret-operator volume that uses the specified SecretClass to
/// request the specified [`SecretClassVolumeProvisionParts`].
///
/// This function forces the caller to make an explicit choice if the public parts are
/// sufficient or if private (e.g. a certificate for the Pod) parts are needed as well.
/// This is done to avoid accidentally requesting too much parts. For details see
/// [this issue](https://github.com/stackabletech/issues/issues/547).
pub fn new(
secret_class: impl Into<String>,
provision_parts: SecretClassVolumeProvisionParts,
) -> Self {
Self {
secret_class: secret_class.into(),
scopes: Vec::new(),
format: None,
kerberos_service_names: Vec::new(),
tls_pkcs12_password: None,
auto_tls_cert_lifetime: None,
provision_parts,
}
}

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

#[rustfmt::skip]
annotations
.insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?);
.insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?)
.insert(Annotation::secret_provision_parts(&self.provision_parts).context(ParseAnnotationSnafu)?);

if !self.scopes.is_empty() {
annotations
Expand Down
39 changes: 34 additions & 5 deletions crates/stackable-operator/src/commons/secret_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ impl SecretClassVolume {

pub fn to_ephemeral_volume_source(
&self,
provision_parts: SecretClassVolumeProvisionParts,
) -> Result<EphemeralVolumeSource, SecretClassVolumeError> {
let mut secret_operator_volume_builder =
SecretOperatorVolumeSourceBuilder::new(&self.secret_class);
SecretOperatorVolumeSourceBuilder::new(&self.secret_class, provision_parts);

if let Some(scope) = &self.scope {
if scope.pod {
Expand All @@ -62,8 +63,12 @@ impl SecretClassVolume {
.context(SecretOperatorVolumeSnafu)
}

pub fn to_volume(&self, volume_name: &str) -> Result<Volume, SecretClassVolumeError> {
let ephemeral = self.to_ephemeral_volume_source()?;
pub fn to_volume(
&self,
volume_name: &str,
provision_parts: SecretClassVolumeProvisionParts,
) -> Result<Volume, SecretClassVolumeError> {
let ephemeral = self.to_ephemeral_volume_source(provision_parts)?;
Ok(VolumeBuilder::new(volume_name).ephemeral(ephemeral).build())
}
}
Expand Down Expand Up @@ -94,14 +99,33 @@ pub struct SecretClassVolumeScope {
pub listener_volumes: Vec<String>,
}

/// What parts of secret material should be provisioned into the requested volume.
//
// There intentionally isn't a global [`Default`] impl, as it is secret-operator's concern what it
// chooses as a default.
// TODO (@Techassi): This to me is a HUGE indicator this lives in the wrong place. All these secret
// volume builders/helpers should be defined as part of a secret-operator library to be as close as
// possible to secret-operator, which is the authoritative source of truth for all of this.
#[derive(Copy, Clone, Debug, PartialEq, Eq, strum::AsRefStr)]
#[strum(serialize_all = "kebab-case")]
pub enum SecretClassVolumeProvisionParts {
/// Only provision public parts, such as the CA certificate (either as PEM or truststore) or
/// `krb5.conf`.
Public,

/// Provision all parts, which includes all [`Public`](Self::Public) ones as well as additional
/// private parts, such as a TLS cert + private key, a keystore or a keytab.
PublicPrivate,
}

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use super::*;

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

let expected_volume_attributes = BTreeMap::from([
Expand All @@ -123,6 +148,10 @@ mod tests {
"secrets.stackable.tech/scope".to_string(),
"pod,service=myservice,listener-volume=mylistener".to_string(),
),
(
"secrets.stackable.tech/provision-parts".to_string(),
"public-private".to_string(),
),
]);

assert_eq!(
Expand Down
7 changes: 5 additions & 2 deletions crates/stackable-operator/src/commons/tls_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use crate::{
self,
pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder},
},
commons::secret_class::{SecretClassVolume, SecretClassVolumeError},
commons::secret_class::{
SecretClassVolume, SecretClassVolumeError, SecretClassVolumeProvisionParts,
},
constants::secret::SECRET_BASE_PATH,
};

Expand Down Expand Up @@ -72,7 +74,8 @@ impl TlsClientDetails {
let volume_name = format!("{secret_class}-ca-cert");
let secret_class_volume = SecretClassVolume::new(secret_class.clone(), None);
let volume = secret_class_volume
.to_volume(&volume_name)
// We only need the public CA cert
.to_volume(&volume_name, SecretClassVolumeProvisionParts::Public)
.context(SecretClassVolumeSnafu)?;

volumes.push(volume);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use crate::{
self,
pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder},
},
commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError},
commons::{
secret_class::{SecretClassVolumeError, SecretClassVolumeProvisionParts},
tls_verification::TlsClientDetailsError,
},
constants::secret::SECRET_BASE_PATH,
crd::authentication::ldap::v1alpha1::{AuthenticationProvider, FieldNames},
};
Expand Down Expand Up @@ -94,7 +97,8 @@ impl AuthenticationProvider {
let secret_class = &bind_credentials.secret_class;
let volume_name = format!("{secret_class}-bind-credentials");
let volume = bind_credentials
.to_volume(&volume_name)
// We need the private LDAP bind credentials
.to_volume(&volume_name, SecretClassVolumeProvisionParts::PublicPrivate)
.context(BindCredentialsSnafu)?;

volumes.push(volume);
Expand Down Expand Up @@ -234,7 +238,10 @@ mod tests {
secret_class: "ldap-ca-cert".to_string(),
scope: None,
}
.to_volume("ldap-ca-cert-ca-cert")
.to_volume(
"ldap-ca-cert-ca-cert",
SecretClassVolumeProvisionParts::Public
)
.unwrap()
]
);
Expand Down Expand Up @@ -263,13 +270,19 @@ mod tests {
secret_class: "openldap-bind-credentials".to_string(),
scope: None,
}
.to_volume("openldap-bind-credentials-bind-credentials")
.to_volume(
"openldap-bind-credentials-bind-credentials",
SecretClassVolumeProvisionParts::PublicPrivate
)
.unwrap(),
SecretClassVolume {
secret_class: "ldap-ca-cert".to_string(),
scope: None,
}
.to_volume("ldap-ca-cert-ca-cert")
.to_volume(
"ldap-ca-cert-ca-cert",
SecretClassVolumeProvisionParts::Public
)
.unwrap()
]
);
Expand Down
14 changes: 10 additions & 4 deletions crates/stackable-operator/src/crd/s3/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,16 @@ mod tests {
.unwrap()
.annotations
.unwrap(),
&BTreeMap::from([(
"secrets.stackable.tech/class".to_string(),
"ionos-s3-credentials".to_string()
)]),
&BTreeMap::from([
(
"secrets.stackable.tech/class".to_string(),
"ionos-s3-credentials".to_string()
),
(
"secrets.stackable.tech/provision-parts".to_string(),
"public-private".to_string()
)
]),
);

assert_eq!(mount.name, volume.name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use url::Url;
use crate::{
builder::pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder},
client::Client,
commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError},
commons::{
secret_class::{SecretClassVolumeError, SecretClassVolumeProvisionParts},
tls_verification::TlsClientDetailsError,
},
constants::secret::SECRET_BASE_PATH,
crd::s3::{
connection::ResolvedConnection,
Expand Down Expand Up @@ -110,7 +113,8 @@ impl ConnectionSpec {

volumes.push(
credentials
.to_volume(&volume_name)
// We need the private S3 credentials
.to_volume(&volume_name, SecretClassVolumeProvisionParts::PublicPrivate)
.context(AddS3CredentialVolumesSnafu)?,
);
mounts.push(
Expand Down
10 changes: 10 additions & 0 deletions crates/stackable-operator/src/kvp/annotation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use delegate::delegate;

use crate::{
builder::pod::volume::SecretOperatorVolumeScope,
commons::secret_class::SecretClassVolumeProvisionParts,
iter::TryFromIterator,
kvp::{Key, KeyValuePair, KeyValuePairError, KeyValuePairs, KeyValuePairsError},
};
Expand Down Expand Up @@ -80,6 +81,15 @@ impl Annotation {
self.0
}

/// Constructs a `secrets.stackable.tech/provision-parts` annotation.
pub fn secret_provision_parts(
provision_parts: &SecretClassVolumeProvisionParts,
) -> Result<Self, AnnotationError> {
let kvp =
KeyValuePair::try_from(("secrets.stackable.tech/provision-parts", provision_parts))?;
Ok(Self(kvp))
}

/// Constructs a `secrets.stackable.tech/class` annotation.
pub fn secret_class(secret_class: &str) -> Result<Self, AnnotationError> {
let kvp = KeyValuePair::try_from(("secrets.stackable.tech/class", secret_class))?;
Expand Down
Loading