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
12 changes: 12 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion 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 @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>
Expand Down
34 changes: 30 additions & 4 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,6 +99,22 @@ pub struct SecretClassVolumeScope {
pub listener_volumes: Vec<String>,
}

/// 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;
Expand All @@ -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([
Expand All @@ -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!(
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