diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 64d9bde2c..425ab61c0 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### 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 + ## [0.106.2] - 2026-02-26 ### Changed diff --git a/crates/stackable-operator/src/builder/pod/volume.rs b/crates/stackable-operator/src/builder/pod/volume.rs index 9b7a1bd98..d5a53e965 100644 --- a/crates/stackable-operator/src/builder/pod/volume.rs +++ b/crates/stackable-operator/src/builder/pod/volume.rs @@ -14,6 +14,7 @@ use tracing::warn; use crate::{ builder::meta::ObjectMetaBuilder, + commons::secret_class::SecretClassVolumeProvisionParts, kvp::{Annotation, AnnotationError, Annotations, LabelError, Labels}, }; @@ -281,10 +282,21 @@ pub struct SecretOperatorVolumeSourceBuilder { kerberos_service_names: Vec, tls_pkcs12_password: Option, auto_tls_cert_lifetime: Option, + provision_parts: SecretClassVolumeProvisionParts, } impl SecretOperatorVolumeSourceBuilder { - pub fn new(secret_class: impl Into) -> 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, + provision_parts: SecretClassVolumeProvisionParts, + ) -> Self { Self { secret_class: secret_class.into(), scopes: Vec::new(), @@ -292,6 +304,7 @@ impl SecretOperatorVolumeSourceBuilder { kerberos_service_names: Vec::new(), tls_pkcs12_password: None, auto_tls_cert_lifetime: None, + provision_parts, } } @@ -342,6 +355,10 @@ impl SecretOperatorVolumeSourceBuilder { annotations .insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?); + annotations.insert( + Annotation::secret_provision_parts(&self.provision_parts) + .context(ParseAnnotationSnafu)?, + ); if !self.scopes.is_empty() { annotations diff --git a/crates/stackable-operator/src/client.rs b/crates/stackable-operator/src/client.rs index bb9ceb122..3c27863ba 100644 --- a/crates/stackable-operator/src/client.rs +++ b/crates/stackable-operator/src/client.rs @@ -590,7 +590,7 @@ impl Client { pub trait GetApi: Resource + Sized { /// The namespace type for `Self`'s scope. /// - /// This will be [`str`] for namespaced resource, and [`()`] for cluster-scoped resources. + /// This will be [`str`] for namespaced resource, and `()` for cluster-scoped resources. type Namespace: ?Sized; /// Get a [`kube::Api`] for `Self`'s native scope.. fn get_api(client: kube::Client, ns: &Self::Namespace) -> kube::Api diff --git a/crates/stackable-operator/src/commons/secret_class.rs b/crates/stackable-operator/src/commons/secret_class.rs index 2ed8ca7de..ddd48075e 100644 --- a/crates/stackable-operator/src/commons/secret_class.rs +++ b/crates/stackable-operator/src/commons/secret_class.rs @@ -38,9 +38,10 @@ impl SecretClassVolume { pub fn to_ephemeral_volume_source( &self, + provision_parts: SecretClassVolumeProvisionParts, ) -> Result { 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 { @@ -62,8 +63,12 @@ impl SecretClassVolume { .context(SecretOperatorVolumeSnafu) } - pub fn to_volume(&self, volume_name: &str) -> Result { - let ephemeral = self.to_ephemeral_volume_source()?; + pub fn to_volume( + &self, + volume_name: &str, + provision_parts: SecretClassVolumeProvisionParts, + ) -> Result { + let ephemeral = self.to_ephemeral_volume_source(provision_parts)?; Ok(VolumeBuilder::new(volume_name).ephemeral(ephemeral).build()) } } @@ -94,6 +99,22 @@ pub struct SecretClassVolumeScope { pub listener_volumes: Vec, } +/// What parts secret-operator should provision into the requested volume. +// +// There intentionally isn't a global [`Default`] impl, as it's secret-ops concern what it chooses +// as a default. +#[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; @@ -111,7 +132,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([ @@ -123,6 +145,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!( diff --git a/crates/stackable-operator/src/commons/tls_verification.rs b/crates/stackable-operator/src/commons/tls_verification.rs index 1e399b0cf..e0b97b440 100644 --- a/crates/stackable-operator/src/commons/tls_verification.rs +++ b/crates/stackable-operator/src/commons/tls_verification.rs @@ -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, }; @@ -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); diff --git a/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs index 674ddbd91..d0dbcc990 100644 --- a/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs @@ -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}, }; @@ -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); @@ -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() ] ); @@ -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() ] ); diff --git a/crates/stackable-operator/src/crd/s3/connection/mod.rs b/crates/stackable-operator/src/crd/s3/connection/mod.rs index b41c280d8..bf0ccad7e 100644 --- a/crates/stackable-operator/src/crd/s3/connection/mod.rs +++ b/crates/stackable-operator/src/crd/s3/connection/mod.rs @@ -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); diff --git a/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs index 0c6c1efd7..78f5c701f 100644 --- a/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs @@ -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, @@ -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( diff --git a/crates/stackable-operator/src/kvp/annotation/mod.rs b/crates/stackable-operator/src/kvp/annotation/mod.rs index efe7ec388..402fa362b 100644 --- a/crates/stackable-operator/src/kvp/annotation/mod.rs +++ b/crates/stackable-operator/src/kvp/annotation/mod.rs @@ -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}, }; @@ -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 { + 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 { let kvp = KeyValuePair::try_from(("secrets.stackable.tech/class", secret_class))?;