diff --git a/Cargo.toml b/Cargo.toml index 40df4ebfe..cc42d01dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = "0.1.37" tracing-opentelemetry = "0.21.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +url = "2.4.1" [dev-dependencies] rstest = "0.18.1" diff --git a/src/commons/mod.rs b/src/commons/mod.rs index a77a0915c..e16f493c3 100644 --- a/src/commons/mod.rs +++ b/src/commons/mod.rs @@ -1,4 +1,4 @@ -//! This module provides common datastructures or CRDs shared between all the operators +//! This module provides common data-structures or CRDs shared between all the operators pub mod affinity; pub mod authentication; @@ -11,3 +11,4 @@ pub mod rbac; pub mod resources; pub mod s3; pub mod secret_class; +pub mod tls; diff --git a/src/commons/s3.rs b/src/commons/s3.rs deleted file mode 100644 index f2a7e9cb1..000000000 --- a/src/commons/s3.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! Implementation of the bucket definition as described in -//! the -//! -//! Operator CRDs are expected to use the [S3BucketDef] as an entry point to this module -//! and obtain an [InlinedS3BucketSpec] by calling [`S3BucketDef::resolve`]. -//! -use crate::commons::{authentication::tls::Tls, secret_class::SecretClassVolume}; -use crate::error; -use crate::{client::Client, error::OperatorResult}; -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// S3 bucket specification containing only the bucket name and an inlined or -/// referenced connection specification. -#[derive( - Clone, CustomResource, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, -)] -#[kube( - group = "s3.stackable.tech", - version = "v1alpha1", - kind = "S3Bucket", - plural = "s3buckets", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ), - namespaced -)] -#[serde(rename_all = "camelCase")] -pub struct S3BucketSpec { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bucket_name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub connection: Option, -} - -impl S3BucketSpec { - /// Convenience function to retrieve the spec of a S3 bucket resource from the K8S API service. - pub async fn get( - resource_name: &str, - client: &Client, - namespace: &str, - ) -> OperatorResult { - client - .get::(resource_name, namespace) - .await - .map(|crd| crd.spec) - .map_err(|_source| error::Error::MissingS3Bucket { - name: resource_name.to_string(), - }) - } - - /// Map &self to an [InlinedS3BucketSpec] by obtaining connection spec from the K8S API service if necessary - pub async fn inlined( - &self, - client: &Client, - namespace: &str, - ) -> OperatorResult { - match self.connection.as_ref() { - Some(connection_def) => Ok(InlinedS3BucketSpec { - connection: Some(connection_def.resolve(client, namespace).await?), - bucket_name: self.bucket_name.clone(), - }), - None => Ok(InlinedS3BucketSpec { - bucket_name: self.bucket_name.clone(), - connection: None, - }), - } - } -} - -/// Convenience struct with the connection spec inlined. -pub struct InlinedS3BucketSpec { - pub bucket_name: Option, - pub connection: Option, -} - -impl InlinedS3BucketSpec { - /// Build the endpoint URL from [S3ConnectionSpec::host] and [S3ConnectionSpec::port] and the S3 implementation to use - pub fn endpoint(&self) -> Option { - self.connection - .as_ref() - .and_then(|connection| connection.endpoint()) - } -} - -/// Operators are expected to define fields for this type in order to work with S3 buckets. -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum S3BucketDef { - Inline(S3BucketSpec), - Reference(String), -} - -impl S3BucketDef { - /// Returns an [InlinedS3BucketSpec]. - pub async fn resolve( - &self, - client: &Client, - namespace: &str, - ) -> OperatorResult { - match self { - S3BucketDef::Inline(s3_bucket) => s3_bucket.inlined(client, namespace).await, - S3BucketDef::Reference(s3_bucket) => { - S3BucketSpec::get(s3_bucket.as_str(), client, namespace) - .await? - .inlined(client, namespace) - .await - } - } - } -} - -/// Operators are expected to define fields for this type in order to work with S3 connections. -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum S3ConnectionDef { - Inline(S3ConnectionSpec), - Reference(String), -} - -impl S3ConnectionDef { - /// Returns an [S3ConnectionSpec]. - pub async fn resolve( - &self, - client: &Client, - namespace: &str, - ) -> OperatorResult { - match self { - S3ConnectionDef::Inline(s3_connection_spec) => Ok(s3_connection_spec.clone()), - S3ConnectionDef::Reference(s3_conn_reference) => { - S3ConnectionSpec::get(s3_conn_reference, client, namespace).await - } - } - } -} - -/// S3 connection definition as CRD. -#[derive( - CustomResource, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, -)] -#[kube( - group = "s3.stackable.tech", - version = "v1alpha1", - kind = "S3Connection", - plural = "s3connections", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ), - namespaced -)] -#[serde(rename_all = "camelCase")] -pub struct S3ConnectionSpec { - /// Hostname of the S3 server without any protocol or port - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host: Option, - /// Port the S3 server listens on. - /// If not specified the products will determine the port to use. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub port: Option, - /// Which access style to use. - /// Defaults to virtual hosted-style as most of the data products out there. - /// Have a look at the official documentation on - #[serde(default, skip_serializing_if = "Option::is_none")] - pub access_style: Option, - /// If the S3 uses authentication you have to specify you S3 credentials. - /// In the most cases a SecretClass providing `accessKey` and `secretKey` is sufficient. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub credentials: Option, - /// If you want to use TLS when talking to S3 you can enable TLS encrypted communication with this setting. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tls: Option, -} - -impl S3ConnectionSpec { - /// Convenience function to retrieve the spec of a S3 connection resource from the K8S API service. - pub async fn get( - resource_name: &str, - client: &Client, - namespace: &str, - ) -> OperatorResult { - client - .get::(resource_name, namespace) - .await - .map(|conn| conn.spec) - .map_err(|_source| error::Error::MissingS3Connection { - name: resource_name.to_string(), - }) - } - - /// Build the endpoint URL from this connection - pub fn endpoint(&self) -> Option { - let protocol = match self.tls.as_ref() { - Some(_tls) => "https", - _ => "http", - }; - self.host.as_ref().map(|h| match self.port { - Some(p) => format!("{protocol}://{h}:{p}"), - None => format!("{protocol}://{h}"), - }) - } -} - -#[derive( - strum::Display, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, -)] -#[strum(serialize_all = "PascalCase")] -pub enum S3AccessStyle { - /// Use path-style access as described in - Path, - /// Use as virtual hosted-style access as described in - #[default] - VirtualHosted, -} - -#[cfg(test)] -mod test { - use std::str; - - use crate::commons::s3::{S3AccessStyle, S3ConnectionDef}; - use crate::commons::s3::{S3BucketSpec, S3ConnectionSpec}; - use crate::yaml; - - #[test] - fn test_ser_inline() { - let bucket = S3BucketSpec { - bucket_name: Some("test-bucket-name".to_owned()), - connection: Some(S3ConnectionDef::Inline(S3ConnectionSpec { - host: Some("host".to_owned()), - port: Some(8080), - credentials: None, - access_style: Some(S3AccessStyle::VirtualHosted), - tls: None, - })), - }; - - let mut buf = Vec::new(); - yaml::serialize_to_explicit_document(&mut buf, &bucket).expect("serializable value"); - let actual_yaml = str::from_utf8(&buf).expect("UTF-8 encoded document"); - - let expected_yaml = "--- -bucketName: test-bucket-name -connection: - inline: - host: host - port: 8080 - accessStyle: VirtualHosted -"; - - assert_eq!(expected_yaml, actual_yaml) - } -} diff --git a/src/commons/s3/crd.rs b/src/commons/s3/crd.rs new file mode 100644 index 000000000..4e43e8773 --- /dev/null +++ b/src/commons/s3/crd.rs @@ -0,0 +1,81 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::commons::{secret_class::SecretClassVolume, tls::TlsClientDetails}; + +use super::S3ConnectionInlineOrReference; + +/// Contains connection and access details to access an S3 object store. +#[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[kube( + group = "s3.stackable.tech", + version = "v1alpha1", + kind = "S3Connection", + plural = "s3connections", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ), + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct S3ConnectionSpec { + /// Hostname of the S3 server without any protocol or port. + // TODO: Rename to `hostname` to be more consistent with other structs. + #[serde(rename = "host")] + pub hostname: String, + + /// Port the S3 server listens on. + /// Port of the S3 server. If TLS is used defaults to 443 otherwise to 80. + pub(crate) port: Option, + + /// Which access style to use. + /// Defaults to virtual hosted-style as most of the data products out there. + /// Have a look at the official documentation on + #[serde(default)] + pub access_style: S3AccessStyle, + + /// If the S3 uses authentication you have to specify you S3 credentials. + /// In the most cases a SecretClass providing `accessKey` and `secretKey` is sufficient. + pub credentials: Option, + + /// If you want to use TLS when talking to S3 you can enable TLS encrypted communication with this setting. + #[serde(flatten)] + pub tls: TlsClientDetails, +} + +/// Contains the name of the bucket as well as the needed connection details. +#[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[kube( + group = "s3.stackable.tech", + version = "v1alpha1", + kind = "S3Bucket", + plural = "s3buckets", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ), + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct S3BucketSpec { + /// Name of the bucket + pub(crate) bucket_name: String, + /// Either a inlined s3 connection or a reference to a S3Connection object + pub(crate) connection: S3ConnectionInlineOrReference, +} + +#[derive( + strum::Display, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, +)] +#[strum(serialize_all = "PascalCase")] +pub enum S3AccessStyle { + /// Use path-style access as described in + Path, + /// Use as virtual hosted-style access as described in + #[default] + VirtualHosted, +} diff --git a/src/commons/s3/helpers.rs b/src/commons/s3/helpers.rs new file mode 100644 index 000000000..07096aaf7 --- /dev/null +++ b/src/commons/s3/helpers.rs @@ -0,0 +1,260 @@ +use k8s_openapi::api::core::v1::{Volume, VolumeMount}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use snafu::ResultExt; +use url::Url; + +use crate::{ + builder::{ContainerBuilder, PodBuilder, VolumeMountBuilder}, + client::Client, + commons::{ + s3::{ + ParseS3EndpointSnafu, RetrieveS3ConnectionSnafu, S3Bucket, S3BucketSpec, S3Connection, + S3ConnectionSpec, S3Result, SetS3EndpointSchemeSnafu, + }, + tls::SECRET_BASE_PATH, + }, +}; + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +// TODO: This probably should be serde(untagged), but this would be a breaking change +pub enum S3ConnectionInlineOrReference { + Inline(S3ConnectionSpec), + Reference(String), +} + +/// Use this type in you operator! +pub type ResolvedS3Connection = S3ConnectionSpec; + +impl S3ConnectionInlineOrReference { + pub async fn resolve(self, client: &Client, namespace: &str) -> S3Result { + match self { + Self::Inline(inline) => Ok(inline), + Self::Reference(reference) => Ok(client + .get::(&reference, namespace) + .await + .context(RetrieveS3ConnectionSnafu)? + .spec), + } + } +} + +impl ResolvedS3Connection { + /// Build the endpoint URL from this connection + pub fn endpoint(&self) -> S3Result { + let mut url = Url::parse(&format!("http://{}:{}", self.hostname, self.port())) + .context(ParseS3EndpointSnafu)?; + + if self.tls.use_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.use_tls() { 443 } else { 80 }) + } + + /// This functions adds + /// + /// * Credentials needed to connect to S3 + pub fn add_volumes_and_mounts( + &self, + pod_builder: &mut PodBuilder, + container_builders: Vec<&mut ContainerBuilder>, + ) { + let (volumes, mounts) = self.volumes_and_mounts(); + pod_builder.add_volumes(volumes); + for cb in container_builders { + cb.add_volume_mounts(mounts.clone()); + } + } + + /// 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) -> (Vec, Vec) { + 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)); + mounts.push( + VolumeMountBuilder::new(volume_name, format!("{SECRET_BASE_PATH}/{secret_class}")) + .build(), + ); + } + + (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"), + ) + }) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +// TODO: This probably should be serde(untagged), but this would be a breaking change +pub enum S3BucketInlineOrReference { + Inline(S3BucketSpec), + Reference(String), +} + +/// Use this struct in your operator. +pub struct ResolvedS3Bucket { + pub bucket_name: String, + pub connection: S3ConnectionSpec, +} + +impl S3BucketInlineOrReference { + pub async fn resolve(self, client: &Client, namespace: &str) -> S3Result { + match self { + Self::Inline(inline) => Ok(ResolvedS3Bucket { + bucket_name: inline.bucket_name, + connection: inline.connection.resolve(client, namespace).await?, + }), + Self::Reference(reference) => { + let bucket = client + .get::(&reference, namespace) + .await + .context(RetrieveS3ConnectionSnafu)? + .spec; + Ok(ResolvedS3Bucket { + bucket_name: bucket.bucket_name, + connection: bucket.connection.resolve(client, namespace).await?, + }) + } + } + } +} + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + + use crate::commons::{ + secret_class::SecretClassVolume, + tls::{CaCert, Tls, TlsClientDetails, TlsServerVerification, TlsVerification}, + }; + + use super::*; + + // We cant test the correct resolve, as we can't mock the k8s API. + + #[test] + fn test_http() { + let s3 = ResolvedS3Connection { + hostname: "minio".to_string(), + port: None, + access_style: Default::default(), + credentials: None, + tls: TlsClientDetails { tls: None }, + }; + let (volumes, mounts) = s3.volumes_and_mounts(); + + assert_eq!(s3.endpoint().unwrap(), Url::parse("http://minio").unwrap()); + assert_eq!(volumes, vec![]); + assert_eq!(mounts, vec![]); + } + + #[test] + fn test_https() { + let s3 = ResolvedS3Connection { + hostname: "s3-eu-central-2.ionoscloud.com".to_string(), + port: None, + access_style: Default::default(), + credentials: Some(SecretClassVolume { + secret_class: "ionos-s3-credentials".to_string(), + scope: None, + }), + tls: TlsClientDetails { + tls: Some(Tls { + verification: TlsVerification::Server(TlsServerVerification { + ca_cert: CaCert::WebPki {}, + }), + }), + }, + }; + let (mut volumes, mut mounts) = s3.volumes_and_mounts(); + + assert_eq!( + s3.endpoint().unwrap(), + Url::parse("https://s3-eu-central-2.ionoscloud.com").unwrap() + ); + assert_eq!(volumes.len(), 1); + let volume = volumes.remove(0); + assert_eq!(mounts.len(), 1); + let mount = mounts.remove(0); + + assert_eq!(&volume.name, "ionos-s3-credentials-s3-credentials"); + assert_eq!( + &volume + .ephemeral + .unwrap() + .volume_claim_template + .unwrap() + .metadata + .unwrap() + .annotations + .unwrap(), + &BTreeMap::from([( + "secrets.stackable.tech/class".to_string(), + "ionos-s3-credentials".to_string() + )]), + ); + + assert_eq!(mount.name, volume.name); + assert_eq!(mount.mount_path, "/stackable/secrets/ionos-s3-credentials"); + assert_eq!( + s3.credentials_mount_paths(), + Some(( + "/stackable/secrets/ionos-s3-credentials/accessKey".to_string(), + "/stackable/secrets/ionos-s3-credentials/secretKey".to_string() + )) + ); + } + + #[test] + fn test_https_without_verification() { + let s3 = ResolvedS3Connection { + hostname: "minio".to_string(), + port: Some(1234), + access_style: Default::default(), + credentials: None, + tls: TlsClientDetails { + tls: Some(Tls { + verification: crate::commons::tls::TlsVerification::None {}, + }), + }, + }; + let (volumes, mounts) = s3.volumes_and_mounts(); + + assert_eq!( + s3.endpoint().unwrap(), + Url::parse("https://minio:1234").unwrap() + ); + assert_eq!(volumes, vec![]); + assert_eq!(mounts, vec![]); + } +} diff --git a/src/commons/s3/mod.rs b/src/commons/s3/mod.rs new file mode 100644 index 000000000..621936ea2 --- /dev/null +++ b/src/commons/s3/mod.rs @@ -0,0 +1,25 @@ +mod crd; +mod helpers; + +pub use crd::*; +pub use helpers::*; + +use snafu::Snafu; +use url::Url; + +#[derive(Debug, Snafu)] +pub enum S3Error { + #[snafu(display("failed to retrieve S3 connection"))] + RetrieveS3Connection { source: crate::error::Error }, + + #[snafu(display("failed to retrieve S3 bucket"))] + RetrieveS3Bucket { source: crate::error::Error }, + + #[snafu(display("failed to parse S3 endpoint"))] + ParseS3Endpoint { source: url::ParseError }, + + #[snafu(display("failed to set S3 endpoint scheme '{scheme}' for endpoint '{endpoint}'"))] + SetS3EndpointScheme { endpoint: Url, scheme: String }, +} + +pub type S3Result = std::result::Result; diff --git a/src/commons/tls.rs b/src/commons/tls.rs new file mode 100644 index 000000000..b80d38025 --- /dev/null +++ b/src/commons/tls.rs @@ -0,0 +1,147 @@ +use k8s_openapi::api::core::v1::{Volume, VolumeMount}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::builder::{ContainerBuilder, PodBuilder, VolumeMountBuilder}; + +use super::secret_class::SecretClassVolume; + +pub const SECRET_BASE_PATH: &str = "/stackable/secrets"; + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TlsClientDetails { + /// Use a TLS connection. If not specified no TLS will be used + pub tls: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Tls { + /// The verification method used to verify the certificates of the server and/or the client + pub verification: TlsVerification, +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TlsVerification { + /// Use TLS but don't verify certificates + None {}, + /// Use TLS and ca certificate to verify the server + Server(TlsServerVerification), +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TlsServerVerification { + /// Ca cert to verify the server + pub ca_cert: CaCert, +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CaCert { + /// Use TLS and the ca certificates trusted by the common web browsers to verify the server. + /// This can be useful when you e.g. use public AWS S3 or other public available services. + WebPki {}, + /// Name of the SecretClass which will provide the ca cert. + /// Note that a SecretClass does not need to have a key but can also work with just a ca cert. + /// So if you got provided with a ca cert but don't have access to the key you can still use this method. + SecretClass(String), +} + +impl TlsClientDetails { + /// This functions adds + /// + /// * The needed volumes to the PodBuilder + /// * The needed volume_mounts to all the ContainerBuilder in the list (e.g. init + main container) + /// + /// This function will handle + /// + /// * Tls secret class used to verify the cert of the LDAP server + pub fn add_volumes_and_mounts( + &self, + pod_builder: &mut PodBuilder, + container_builders: Vec<&mut ContainerBuilder>, + ) { + let (volumes, mounts) = self.volumes_and_mounts(); + pod_builder.add_volumes(volumes); + for cb in container_builders { + cb.add_volume_mounts(mounts.clone()); + } + } + + /// 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) -> (Vec, Vec) { + let mut volumes = Vec::new(); + let mut mounts = Vec::new(); + + if let Some(secret_class) = self.tls_ca_cert_secret_class() { + let volume_name = format!("{secret_class}-ca-cert"); + volumes.push( + SecretClassVolume { + secret_class: secret_class.to_string(), + scope: None, + } + .to_volume(&volume_name), + ); + mounts.push( + VolumeMountBuilder::new(volume_name, format!("{SECRET_BASE_PATH}/{secret_class}")) + .build(), + ); + } + + (volumes, mounts) + } + + /// Whether TLS is configured + pub const fn use_tls(&self) -> bool { + self.tls.is_some() + } + + /// Whether TLS verification is configured. Returns false if TLS itself isn't configured + pub fn use_tls_verification(&self) -> bool { + self.tls + .as_ref() + .map(|tls| tls.verification != TlsVerification::None {}) + .unwrap_or_default() + } + + /// Returns the path of the ca.crt that should be used to verify the LDAP server certificate + /// if TLS verification with a CA cert from a SecretClass is configured. + pub fn tls_ca_cert_mount_path(&self) -> Option { + self.tls_ca_cert_secret_class() + .map(|secret_class| format!("{SECRET_BASE_PATH}/{secret_class}/ca.crt")) + } + + /// Extracts the SecretClass that provides the CA cert used to verify the server certificate. + fn tls_ca_cert_secret_class(&self) -> Option { + if let Some(Tls { + verification: + TlsVerification::Server(TlsServerVerification { + ca_cert: CaCert::SecretClass(secret_class), + }), + }) = &self.tls + { + Some(secret_class.to_owned()) + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use crate::commons::authentication::{ + tls::TlsAuthenticationProvider, AuthenticationClassProvider, + }; + + #[test] + fn test_authentication_class_provider_to_string() { + let tls_provider = AuthenticationClassProvider::Tls(TlsAuthenticationProvider { + client_cert_secret_class: None, + }); + assert_eq!("Tls", tls_provider.to_string()) + } +}