diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index ec0fdd5e..47b9af15 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -12,7 +12,12 @@ All notable changes to this project will be documented in this file. - The `static` authentication provider must now be imported using `r#static`. - Import are now more granular in general. +### Fixed + +- Re-export versioned CRD-specific error types ([#1025]). + [#968]: https://github.com/stackabletech/operator-rs/pull/968 +[#1025]: https://github.com/stackabletech/operator-rs/pull/1025 ## [0.92.0] - 2025-04-14 diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index 35ec3dca..8c79f612 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -1,15 +1,19 @@ use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use stackable_versioned::versioned; + +use crate::versioned::versioned; mod v1alpha1_impl; #[versioned(version(name = "v1alpha1"))] pub mod versioned { - // This makes v1alpha1 versions of all authentication providers available to the - // AuthenticationClassProvider enum below. - mod v1alpha1 { + pub mod v1alpha1 { + // Re-export the v1alpha1-specific error type from the private impl module. + pub use v1alpha1_impl::Error; + + // This makes v1alpha1 versions of all authentication providers available to the + // AuthenticationClassProvider enum below. use crate::crd::authentication::{kerberos, ldap, oidc, r#static, tls}; } /// The Stackable Platform uses the AuthenticationClass as a central mechanism to handle user @@ -79,18 +83,20 @@ pub mod versioned { Oidc(oidc::v1alpha1::AuthenticationProvider), /// The [TLS provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_tls). - /// The TLS AuthenticationClass is used when users should authenticate themselves with a TLS certificate. + /// The TLS AuthenticationClass is used when users should authenticate themselves with a + /// TLS certificate. Tls(tls::v1alpha1::AuthenticationProvider), /// The [Kerberos provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_kerberos). - /// The Kerberos AuthenticationClass is used when users should authenticate themselves via Kerberos. + /// The Kerberos AuthenticationClass is used when users should authenticate themselves via + /// Kerberos. Kerberos(kerberos::v1alpha1::AuthenticationProvider), } - /// Common [`v1alpha1::ClientAuthenticationDetails`] which is specified at the client/ product - /// cluster level. It provides a name (key) to resolve a particular [`AuthenticationClass`]. - /// Additionally, it provides authentication provider specific configuration (OIDC and LDAP for - /// example). + /// Common client authentication details which is specified at the client/ product cluster level. + /// + /// It provides a name (key) to resolve a particular [`AuthenticationClass`]. Additionally, it + /// provides authentication provider specific configuration (OIDC and LDAP for example). /// /// If the product needs additional (product specific) authentication options, it is recommended /// to wrap this struct and use `#[serde(flatten)]` on the field. @@ -123,19 +129,22 @@ pub mod versioned { pub struct ClientAuthenticationDetails { /// Name of the [AuthenticationClass](https://docs.stackable.tech/home/nightly/concepts/authentication) used to /// authenticate users. - // - // To get the concrete [`AuthenticationClass`], we must resolve it. This resolution can be achieved by using - // [`ClientAuthenticationDetails::resolve_class`]. + /// + /// To get the concrete [`AuthenticationClass`], we must resolve it. This resolution can be + /// achieved by using [`ClientAuthenticationDetails::resolve_class`]. #[serde(rename = "authenticationClass")] authentication_class_ref: String, - /// This field contains OIDC-specific configuration. It is only required in case OIDC is used. + /// This field contains OIDC-specific configuration. It is only required in case OIDC is + /// used. + /// + /// Use [`ClientAuthenticationDetails::oidc_or_error`] to get the value or report an error + /// to the user. // - // Use [`ClientAuthenticationDetails::oidc_or_error`] to get the value or report an error to the user. - // TODO: Ideally we want this to be an enum once other `ClientAuthenticationOptions` are added, so - // that user can not configure multiple options at the same time (yes we are aware that this makes a - // changing the type of an AuthenticationClass harder). - // This is a non-breaking change though :) + // TODO: Ideally we want this to be an enum once other `ClientAuthenticationOptions` are + // added, so that user can not configure multiple options at the same time (yes we are aware + // that this makes a changing the type of an AuthenticationClass harder). This is a + // non-breaking change though :) oidc: Option>, } } diff --git a/crates/stackable-operator/src/crd/authentication/ldap/mod.rs b/crates/stackable-operator/src/crd/authentication/ldap/mod.rs index 114196a7..03ab29af 100644 --- a/crates/stackable-operator/src/crd/authentication/ldap/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/ldap/mod.rs @@ -1,15 +1,22 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use stackable_versioned::versioned; -use crate::commons::{ - networking::HostName, secret_class::SecretClassVolume, tls_verification::TlsClientDetails, +use crate::{ + commons::{ + networking::HostName, secret_class::SecretClassVolume, tls_verification::TlsClientDetails, + }, + versioned::versioned, }; mod v1alpha1_impl; #[versioned(version(name = "v1alpha1"))] pub mod versioned { + pub mod v1alpha1 { + // Re-export the v1alpha1-specific error type from the private impl module. + pub use v1alpha1_impl::Error; + } + #[derive( Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, )] 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 18765d36..1f32d23d 100644 --- a/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs @@ -1,6 +1,6 @@ use k8s_openapi::api::core::v1::{Volume, VolumeMount}; use snafu::{ResultExt as _, Snafu}; -use url::{ParseError, Url}; +use url::Url; use crate::{ builder::{ @@ -22,7 +22,7 @@ pub enum Error { BindCredentials { source: SecretClassVolumeError }, #[snafu(display("failed to parse LDAP endpoint url"))] - ParseLdapEndpointUrl { source: ParseError }, + ParseLdapEndpointUrl { source: url::ParseError }, #[snafu(display("failed to add LDAP TLS client details volumes and volume mounts"))] AddLdapTlsClientDetailsVolumes { source: TlsClientDetailsError }, diff --git a/crates/stackable-operator/src/crd/authentication/oidc/mod.rs b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs index 3885ba0c..7f3fe99a 100644 --- a/crates/stackable-operator/src/crd/authentication/oidc/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs @@ -1,10 +1,12 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use stackable_versioned::versioned; #[cfg(doc)] use url::Url; -use crate::commons::{networking::HostName, tls_verification::TlsClientDetails}; +use crate::{ + commons::{networking::HostName, tls_verification::TlsClientDetails}, + versioned::versioned, +}; mod v1alpha1_impl; @@ -17,6 +19,11 @@ const DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH: &str = "/.well-known/openid-configurat #[versioned(version(name = "v1alpha1"))] pub mod versioned { + pub mod v1alpha1 { + // Re-export the v1alpha1-specific error type from the private impl module. + pub use v1alpha1_impl::Error; + } + /// This struct contains configuration values to configure an OpenID Connect /// (OIDC) authentication class. Required fields are the identity provider /// (IdP) `hostname` and the TLS configuration. The `port` is selected diff --git a/crates/stackable-operator/src/crd/authentication/oidc/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/authentication/oidc/v1alpha1_impl.rs index 6dd9a77c..4b5c182e 100644 --- a/crates/stackable-operator/src/crd/authentication/oidc/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/authentication/oidc/v1alpha1_impl.rs @@ -18,7 +18,6 @@ use crate::{ pub type Result = std::result::Result; -// TODO (@Techassi): Move this into mod.rs #[derive(Debug, PartialEq, Snafu)] pub enum Error { #[snafu(display("failed to parse OIDC endpoint url"))] diff --git a/crates/stackable-operator/src/crd/s3/bucket/mod.rs b/crates/stackable-operator/src/crd/s3/bucket/mod.rs index 6b04b661..63596121 100644 --- a/crates/stackable-operator/src/crd/s3/bucket/mod.rs +++ b/crates/stackable-operator/src/crd/s3/bucket/mod.rs @@ -1,30 +1,17 @@ use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use snafu::Snafu; -use stackable_versioned::versioned; -use crate::crd::s3::{ConnectionError, connection::v1alpha1 as conn_v1alpha1}; +use crate::{crd::s3::connection::v1alpha1 as conn_v1alpha1, versioned::versioned}; mod v1alpha1_impl; -// NOTE (@Techassi): Where should this error be placed? Technically errors can -// change between version, because version-specific impl blocks might need -// different variants or might use a completely different error type. -#[derive(Debug, Snafu)] -pub enum BucketError { - #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] - RetrieveS3Connection { - source: crate::client::Error, - s3_connection: String, - }, - - #[snafu(display("failed to resolve S3 connection"))] - ResolveConnection { source: ConnectionError }, -} - #[versioned(version(name = "v1alpha1"))] pub mod versioned { + pub mod v1alpha1 { + pub use v1alpha1_impl::BucketError; + } + /// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). #[versioned(k8s( diff --git a/crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs index cdff4144..d88b5568 100644 --- a/crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs @@ -1,15 +1,27 @@ //! v1alpha1 specific implementations for S3 buckets. -use snafu::ResultExt as _; +use snafu::{ResultExt as _, Snafu}; use crate::{ client::Client, - crd::s3::bucket::{ - BucketError, ResolveConnectionSnafu, RetrieveS3ConnectionSnafu, - v1alpha1::{InlineBucketOrReference, ResolvedBucket, S3Bucket}, + crd::s3::{ + bucket::v1alpha1::{InlineBucketOrReference, ResolvedBucket, S3Bucket}, + connection::v1alpha1::ConnectionError, }, }; +#[derive(Debug, Snafu)] +pub enum BucketError { + #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] + RetrieveS3Connection { + source: crate::client::Error, + s3_connection: String, + }, + + #[snafu(display("failed to resolve S3 connection"))] + ResolveConnection { source: ConnectionError }, +} + impl InlineBucketOrReference { pub async fn resolve( self, diff --git a/crates/stackable-operator/src/crd/s3/connection/mod.rs b/crates/stackable-operator/src/crd/s3/connection/mod.rs index 51137eb0..98714671 100644 --- a/crates/stackable-operator/src/crd/s3/connection/mod.rs +++ b/crates/stackable-operator/src/crd/s3/connection/mod.rs @@ -1,60 +1,27 @@ use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use snafu::{ResultExt as _, Snafu}; -use stackable_versioned::versioned; -use url::Url; use crate::{ - builder::pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder}, commons::{ - networking::HostName, - secret_class::{SecretClassVolume, SecretClassVolumeError}, - tls_verification::{TlsClientDetails, TlsClientDetailsError}, + networking::HostName, secret_class::SecretClassVolume, tls_verification::TlsClientDetails, }, - constants::secret::SECRET_BASE_PATH, - k8s_openapi::api::core::v1::{Volume, VolumeMount}, + versioned::versioned, }; mod v1alpha1_impl; -// NOTE (@Techassi): Where should this error be placed? Technically errors can -// change between version., because version-specific impl blocks might need -// different variants or might use a completely different error type. -#[derive(Debug, Snafu)] -pub enum ConnectionError { - #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] - RetrieveS3Connection { - source: crate::client::Error, - s3_connection: String, - }, - - #[snafu(display("failed to parse S3 endpoint '{endpoint}'"))] - ParseS3Endpoint { - source: url::ParseError, - endpoint: String, - }, - - #[snafu(display("failed to set S3 endpoint scheme '{scheme}' for endpoint '{endpoint}'"))] - SetS3EndpointScheme { endpoint: Url, scheme: String }, - - #[snafu(display("failed to add S3 credential volumes and volume mounts"))] - AddS3CredentialVolumes { source: SecretClassVolumeError }, - - #[snafu(display("failed to add S3 TLS client details volumes and volume mounts"))] - AddS3TlsClientDetailsVolumes { source: TlsClientDetailsError }, - - #[snafu(display("failed to add required volumes"))] - AddVolumes { source: crate::builder::pod::Error }, - - #[snafu(display("failed to add required volumeMounts"))] - AddVolumeMounts { - source: crate::builder::pod::container::Error, - }, -} +// FIXME (@Techassi): This should be versioned as well, but the macro cannot +// handle new-type structs yet. +/// Use this type in you operator! +pub type ResolvedConnection = v1alpha1::ConnectionSpec; #[versioned(version(name = "v1alpha1"))] pub mod versioned { + pub mod v1alpha1 { + pub use v1alpha1_impl::ConnectionError; + } + /// S3 connection definition as a resource. /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). #[versioned(k8s( @@ -134,108 +101,12 @@ pub mod versioned { } } -// FIXME (@Techassi): This should be versioned as well, but the macro cannot -// handle new-type structs yet. -/// Use this type in you operator! -pub type ResolvedConnection = v1alpha1::ConnectionSpec; - -impl ResolvedConnection { - /// Build the endpoint URL from this connection - pub fn endpoint(&self) -> Result { - let endpoint = format!( - "http://{host}:{port}", - host = self.host.as_url_host(), - port = self.port() - ); - let mut url = Url::parse(&endpoint).context(ParseS3EndpointSnafu { endpoint })?; - - if self.tls.uses_tls() { - url.set_scheme("https").map_err(|_| { - SetS3EndpointSchemeSnafu { - scheme: "https".to_string(), - endpoint: url.clone(), - } - .build() - })?; - } - - Ok(url) - } - - /// Returns the port to be used, which is either user configured or defaulted based upon TLS usage - pub fn port(&self) -> u16 { - self.port - .unwrap_or(if self.tls.uses_tls() { 443 } else { 80 }) - } - - /// This functions adds - /// - /// * Credentials needed to connect to S3 - /// * Needed TLS volumes - pub fn add_volumes_and_mounts( - &self, - pod_builder: &mut PodBuilder, - container_builders: Vec<&mut ContainerBuilder>, - ) -> Result<(), ConnectionError> { - let (volumes, mounts) = self.volumes_and_mounts()?; - pod_builder.add_volumes(volumes).context(AddVolumesSnafu)?; - for cb in container_builders { - cb.add_volume_mounts(mounts.clone()) - .context(AddVolumeMountsSnafu)?; - } - - Ok(()) - } - - /// It is recommended to use [`Self::add_volumes_and_mounts`], this function returns you the - /// volumes and mounts in case you need to add them by yourself. - pub fn volumes_and_mounts(&self) -> Result<(Vec, Vec), ConnectionError> { - let mut volumes = Vec::new(); - let mut mounts = Vec::new(); - - if let Some(credentials) = &self.credentials { - let secret_class = &credentials.secret_class; - let volume_name = format!("{secret_class}-s3-credentials"); - - volumes.push( - credentials - .to_volume(&volume_name) - .context(AddS3CredentialVolumesSnafu)?, - ); - mounts.push( - VolumeMountBuilder::new(volume_name, format!("{SECRET_BASE_PATH}/{secret_class}")) - .build(), - ); - } - - // Add needed TLS volumes - let (tls_volumes, tls_mounts) = self - .tls - .volumes_and_mounts() - .context(AddS3TlsClientDetailsVolumesSnafu)?; - volumes.extend(tls_volumes); - mounts.extend(tls_mounts); - - Ok((volumes, mounts)) - } - - /// Returns the path of the files containing bind user and password. - /// This will be None if there are no credentials for this LDAP connection. - pub fn credentials_mount_paths(&self) -> Option<(String, String)> { - self.credentials.as_ref().map(|bind_credentials| { - let secret_class = &bind_credentials.secret_class; - ( - format!("{SECRET_BASE_PATH}/{secret_class}/accessKey"), - format!("{SECRET_BASE_PATH}/{secret_class}/secretKey"), - ) - }) - } -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; + use url::Url; + use super::*; use crate::commons::{ secret_class::SecretClassVolume, 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 b446d660..29f39335 100644 --- a/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs @@ -1,13 +1,143 @@ -use snafu::ResultExt as _; +use k8s_openapi::api::core::v1::{Volume, VolumeMount}; +use snafu::{ResultExt as _, Snafu}; +use url::Url; use crate::{ + builder::pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder}, client::Client, + commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError}, + constants::secret::SECRET_BASE_PATH, crd::s3::{ - connection::{ConnectionError, ResolvedConnection, RetrieveS3ConnectionSnafu}, - v1alpha1::{InlineConnectionOrReference, Region, S3Connection}, + connection::ResolvedConnection, + v1alpha1::{ConnectionSpec, InlineConnectionOrReference, Region, S3Connection}, }, }; +#[derive(Debug, Snafu)] +pub enum ConnectionError { + #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] + RetrieveS3Connection { + source: crate::client::Error, + s3_connection: String, + }, + + #[snafu(display("failed to parse S3 endpoint '{endpoint}'"))] + ParseS3Endpoint { + source: url::ParseError, + endpoint: String, + }, + + #[snafu(display("failed to set S3 endpoint scheme '{scheme}' for endpoint '{endpoint}'"))] + SetS3EndpointScheme { endpoint: Url, scheme: String }, + + #[snafu(display("failed to add S3 credential volumes and volume mounts"))] + AddS3CredentialVolumes { source: SecretClassVolumeError }, + + #[snafu(display("failed to add S3 TLS client details volumes and volume mounts"))] + AddS3TlsClientDetailsVolumes { source: TlsClientDetailsError }, + + #[snafu(display("failed to add required volumes"))] + AddVolumes { source: crate::builder::pod::Error }, + + #[snafu(display("failed to add required volumeMounts"))] + AddVolumeMounts { + source: crate::builder::pod::container::Error, + }, +} + +impl ConnectionSpec { + /// Build the endpoint URL from this connection + pub fn endpoint(&self) -> Result { + let endpoint = format!( + "http://{host}:{port}", + host = self.host.as_url_host(), + port = self.port() + ); + let mut url = Url::parse(&endpoint).context(ParseS3EndpointSnafu { endpoint })?; + + if self.tls.uses_tls() { + url.set_scheme("https").map_err(|_| { + SetS3EndpointSchemeSnafu { + scheme: "https".to_string(), + endpoint: url.clone(), + } + .build() + })?; + } + + Ok(url) + } + + /// Returns the port to be used, which is either user configured or defaulted based upon TLS usage + pub fn port(&self) -> u16 { + self.port + .unwrap_or(if self.tls.uses_tls() { 443 } else { 80 }) + } + + /// This functions adds + /// + /// * Credentials needed to connect to S3 + /// * Needed TLS volumes + pub fn add_volumes_and_mounts( + &self, + pod_builder: &mut PodBuilder, + container_builders: Vec<&mut ContainerBuilder>, + ) -> Result<(), ConnectionError> { + let (volumes, mounts) = self.volumes_and_mounts()?; + pod_builder.add_volumes(volumes).context(AddVolumesSnafu)?; + for cb in container_builders { + cb.add_volume_mounts(mounts.clone()) + .context(AddVolumeMountsSnafu)?; + } + + Ok(()) + } + + /// It is recommended to use [`Self::add_volumes_and_mounts`], this function returns you the + /// volumes and mounts in case you need to add them by yourself. + pub fn volumes_and_mounts(&self) -> Result<(Vec, Vec), ConnectionError> { + let mut volumes = Vec::new(); + let mut mounts = Vec::new(); + + if let Some(credentials) = &self.credentials { + let secret_class = &credentials.secret_class; + let volume_name = format!("{secret_class}-s3-credentials"); + + volumes.push( + credentials + .to_volume(&volume_name) + .context(AddS3CredentialVolumesSnafu)?, + ); + mounts.push( + VolumeMountBuilder::new(volume_name, format!("{SECRET_BASE_PATH}/{secret_class}")) + .build(), + ); + } + + // Add needed TLS volumes + let (tls_volumes, tls_mounts) = self + .tls + .volumes_and_mounts() + .context(AddS3TlsClientDetailsVolumesSnafu)?; + volumes.extend(tls_volumes); + mounts.extend(tls_mounts); + + Ok((volumes, mounts)) + } + + /// Returns the path of the files containing bind user and password. + /// This will be None if there are no credentials for this LDAP connection. + pub fn credentials_mount_paths(&self) -> Option<(String, String)> { + self.credentials.as_ref().map(|bind_credentials| { + let secret_class = &bind_credentials.secret_class; + ( + format!("{SECRET_BASE_PATH}/{secret_class}/accessKey"), + format!("{SECRET_BASE_PATH}/{secret_class}/secretKey"), + ) + }) + } +} + impl Region { /// Having it as `const &str` as well, so we don't always allocate a [`String`] just for comparisons pub const DEFAULT_REGION_NAME: &str = "us-east-1"; diff --git a/crates/stackable-operator/src/crd/s3/mod.rs b/crates/stackable-operator/src/crd/s3/mod.rs index 625a1062..c5b42b92 100644 --- a/crates/stackable-operator/src/crd/s3/mod.rs +++ b/crates/stackable-operator/src/crd/s3/mod.rs @@ -1,10 +1,6 @@ mod bucket; mod connection; -// Publicly re-export unversioned items, in this case errors. -pub use bucket::BucketError; -pub use connection::ConnectionError; - // Group all v1alpha1 items in one module. pub mod v1alpha1 { pub use super::{bucket::v1alpha1::*, connection::v1alpha1::*};