use k8s_openapi::{ api::core::v1::{ CSIVolumeSource, ConfigMapVolumeSource, DownwardAPIVolumeSource, EmptyDirVolumeSource, EphemeralVolumeSource, HostPathVolumeSource, PersistentVolumeClaim, PersistentVolumeClaimSpec, PersistentVolumeClaimTemplate, PersistentVolumeClaimVolumeSource, ProjectedVolumeSource, SecretVolumeSource, Volume, VolumeMount, VolumeResourceRequirements, }, apimachinery::pkg::api::resource::Quantity, }; use snafu::{ResultExt, Snafu}; use tracing::warn; use crate::{ builder::meta::ObjectMetaBuilder, kvp::{annotation, Annotation, AnnotationError, Annotations, LabelError, Labels}, }; /// A builder to build [`Volume`] objects. May only contain one `volume_source` /// at a time. E.g. a call like `secret` after `empty_dir` will overwrite the /// `empty_dir`. #[derive(Clone, Default)] pub struct VolumeBuilder { name: String, volume_source: VolumeSource, } #[derive(Clone)] pub enum VolumeSource { ConfigMap(ConfigMapVolumeSource), DownwardApi(DownwardAPIVolumeSource), EmptyDir(EmptyDirVolumeSource), HostPath(HostPathVolumeSource), PersistentVolumeClaim(PersistentVolumeClaimVolumeSource), Projected(ProjectedVolumeSource), Secret(SecretVolumeSource), Csi(CSIVolumeSource), Ephemeral(Box<EphemeralVolumeSource>), } impl Default for VolumeSource { fn default() -> Self { Self::EmptyDir(EmptyDirVolumeSource { ..EmptyDirVolumeSource::default() }) } } impl VolumeBuilder { pub fn new(name: impl Into<String>) -> VolumeBuilder { VolumeBuilder { name: name.into(), ..VolumeBuilder::default() } } pub fn config_map(&mut self, config_map: impl Into<ConfigMapVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::ConfigMap(config_map.into()); self } pub fn with_config_map(&mut self, name: impl Into<String>) -> &mut Self { self.volume_source = VolumeSource::ConfigMap(ConfigMapVolumeSource { name: name.into(), ..ConfigMapVolumeSource::default() }); self } pub fn downward_api(&mut self, downward_api: impl Into<DownwardAPIVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::DownwardApi(downward_api.into()); self } pub fn empty_dir(&mut self, empty_dir: impl Into<EmptyDirVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::EmptyDir(empty_dir.into()); self } pub fn with_empty_dir( &mut self, medium: Option<impl Into<String>>, quantity: Option<Quantity>, ) -> &mut Self { self.volume_source = VolumeSource::EmptyDir(EmptyDirVolumeSource { medium: medium.map(|m| m.into()), size_limit: quantity, }); self } pub fn host_path(&mut self, host_path: impl Into<HostPathVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::HostPath(host_path.into()); self } pub fn with_host_path( &mut self, path: impl Into<String>, type_: Option<impl Into<String>>, ) -> &mut Self { self.volume_source = VolumeSource::HostPath(HostPathVolumeSource { path: path.into(), type_: type_.map(|t| t.into()), }); self } pub fn persistent_volume_claim( &mut self, persistent_volume_claim: impl Into<PersistentVolumeClaimVolumeSource>, ) -> &mut Self { self.volume_source = VolumeSource::PersistentVolumeClaim(persistent_volume_claim.into()); self } pub fn with_persistent_volume_claim( &mut self, claim_name: impl Into<String>, read_only: bool, ) -> &mut Self { self.volume_source = VolumeSource::PersistentVolumeClaim(PersistentVolumeClaimVolumeSource { claim_name: claim_name.into(), read_only: Some(read_only), }); self } pub fn projected(&mut self, projected: impl Into<ProjectedVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::Projected(projected.into()); self } pub fn secret(&mut self, secret: impl Into<SecretVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::Secret(secret.into()); self } pub fn with_secret(&mut self, secret_name: impl Into<String>, optional: bool) -> &mut Self { self.volume_source = VolumeSource::Secret(SecretVolumeSource { optional: Some(optional), secret_name: Some(secret_name.into()), ..SecretVolumeSource::default() }); self } pub fn csi(&mut self, csi: impl Into<CSIVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::Csi(csi.into()); self } pub fn ephemeral(&mut self, ephemeral: impl Into<EphemeralVolumeSource>) -> &mut Self { self.volume_source = VolumeSource::Ephemeral(Box::new(ephemeral.into())); self } /// Consumes the Builder and returns a constructed Volume pub fn build(&self) -> Volume { let name = self.name.clone(); match &self.volume_source { VolumeSource::ConfigMap(cm) => Volume { name, config_map: Some(cm.clone()), ..Volume::default() }, VolumeSource::DownwardApi(downward_api) => Volume { name, downward_api: Some(downward_api.clone()), ..Volume::default() }, VolumeSource::EmptyDir(empty_dir) => Volume { name, empty_dir: Some(empty_dir.clone()), ..Volume::default() }, VolumeSource::HostPath(host_path) => Volume { name, host_path: Some(host_path.clone()), ..Volume::default() }, VolumeSource::PersistentVolumeClaim(pvc) => Volume { name, persistent_volume_claim: Some(pvc.clone()), ..Volume::default() }, VolumeSource::Projected(projected) => Volume { name, projected: Some(projected.clone()), ..Volume::default() }, VolumeSource::Secret(secret) => Volume { name, secret: Some(secret.clone()), ..Volume::default() }, VolumeSource::Csi(csi) => Volume { name, csi: Some(csi.clone()), ..Volume::default() }, VolumeSource::Ephemeral(ephemeral) => Volume { name, ephemeral: Some((**ephemeral).clone()), ..Volume::default() }, } } } /// A builder to build [`VolumeMount`] objects. #[derive(Clone, Default)] pub struct VolumeMountBuilder { mount_path: String, mount_propagation: Option<String>, name: String, read_only: Option<bool>, sub_path: Option<String>, sub_path_expr: Option<String>, } impl VolumeMountBuilder { pub fn new(name: impl Into<String>, mount_path: impl Into<String>) -> VolumeMountBuilder { VolumeMountBuilder { mount_path: mount_path.into(), name: name.into(), ..VolumeMountBuilder::default() } } pub fn read_only(&mut self, read_only: bool) -> &mut Self { self.read_only = Some(read_only); self } pub fn mount_propagation(&mut self, mount_propagation: impl Into<String>) -> &mut Self { self.mount_propagation = Some(mount_propagation.into()); self } pub fn sub_path(&mut self, sub_path: impl Into<String>) -> &mut Self { self.sub_path = Some(sub_path.into()); self } pub fn sub_path_expr(&mut self, sub_path_expr: impl Into<String>) -> &mut Self { self.sub_path_expr = Some(sub_path_expr.into()); self } /// Consumes the Builder and returns a constructed VolumeMount pub fn build(&self) -> VolumeMount { VolumeMount { mount_path: self.mount_path.clone(), mount_propagation: self.mount_propagation.clone(), name: self.name.clone(), read_only: self.read_only, sub_path: self.sub_path.clone(), sub_path_expr: self.sub_path_expr.clone(), // This attribute is supported starting with Kubernetes 1.30. // Because we support older Kubernetes versions as well, we can not // use it for now, as we would not work on older Kubernetes clusters. recursive_read_only: None, } } } #[derive(Debug, PartialEq, Snafu)] pub enum SecretOperatorVolumeSourceBuilderError { #[snafu(display("failed to parse secret operator volume annotation"))] ParseAnnotation { source: AnnotationError }, } #[derive(Clone)] pub struct SecretOperatorVolumeSourceBuilder { secret_class: String, scopes: Vec<SecretOperatorVolumeScope>, format: Option<SecretFormat>, kerberos_service_names: Vec<String>, tls_pkcs12_password: Option<String>, } impl SecretOperatorVolumeSourceBuilder { pub fn new(secret_class: impl Into<String>) -> Self { Self { secret_class: secret_class.into(), scopes: Vec::new(), format: None, kerberos_service_names: Vec::new(), tls_pkcs12_password: None, } } pub fn with_node_scope(&mut self) -> &mut Self { self.scopes.push(SecretOperatorVolumeScope::Node); self } pub fn with_pod_scope(&mut self) -> &mut Self { self.scopes.push(SecretOperatorVolumeScope::Pod); self } pub fn with_service_scope(&mut self, name: impl Into<String>) -> &mut Self { self.scopes .push(SecretOperatorVolumeScope::Service { name: name.into() }); self } pub fn with_listener_volume_scope(&mut self, name: impl Into<String>) -> &mut Self { self.scopes .push(SecretOperatorVolumeScope::ListenerVolume { name: name.into() }); self } pub fn with_format(&mut self, format: SecretFormat) -> &mut Self { self.format = Some(format); self } pub fn with_kerberos_service_name(&mut self, name: impl Into<String>) -> &mut Self { self.kerberos_service_names.push(name.into()); self } pub fn with_tls_pkcs12_password(&mut self, password: impl Into<String>) -> &mut Self { self.tls_pkcs12_password = Some(password.into()); self } pub fn build(&self) -> Result<EphemeralVolumeSource, SecretOperatorVolumeSourceBuilderError> { let mut annotations = Annotations::new(); annotations.extend([annotation::well_known::secret_volume::secret_class( &self.secret_class, ) .context(ParseAnnotationSnafu)?]); if !self.scopes.is_empty() { annotations.extend([ annotation::well_known::secret_volume::secret_scope(&self.scopes) .context(ParseAnnotationSnafu)?, ]); } if let Some(format) = &self.format { annotations.extend([annotation::well_known::secret_volume::secret_format( format.as_ref(), ) .context(ParseAnnotationSnafu)?]); } if !self.kerberos_service_names.is_empty() { annotations.extend([ annotation::well_known::secret_volume::kerberos_service_names( &self.kerberos_service_names, ) .context(ParseAnnotationSnafu)?, ]); } if let Some(password) = &self.tls_pkcs12_password { // The `tls_pkcs12_password` is only used for PKCS12 stores. if Some(SecretFormat::TlsPkcs12) != self.format { warn!(format.actual = ?self.format, format.expected = ?Some(SecretFormat::TlsPkcs12), "A TLS PKCS12 password was set but ignored because another format was requested") } else { annotations.extend([annotation::well_known::secret_volume::tls_pkcs12_password( password, ) .context(ParseAnnotationSnafu)?]); } } Ok(EphemeralVolumeSource { volume_claim_template: Some(PersistentVolumeClaimTemplate { metadata: Some(ObjectMetaBuilder::new().annotations(annotations).build()), spec: PersistentVolumeClaimSpec { storage_class_name: Some("secrets.stackable.tech".to_string()), resources: Some(VolumeResourceRequirements { requests: Some([("storage".to_string(), Quantity("1".to_string()))].into()), ..Default::default() }), access_modes: Some(vec!["ReadWriteOnce".to_string()]), ..PersistentVolumeClaimSpec::default() }, }), }) } } /// A [secret format](https://docs.stackable.tech/home/stable/secret-operator/secretclass.html#format) known by secret-operator. /// /// This must either match or be convertible from the corresponding secret class, or provisioning the volume will fail. #[derive(Clone, Debug, PartialEq, Eq, strum::AsRefStr)] #[strum(serialize_all = "kebab-case")] pub enum SecretFormat { /// A TLS certificate formatted as a PEM triple (`ca.crt`, `tls.crt`, `tls.key`) according to Kubernetes conventions. TlsPem, /// A TLS certificate formatted as a PKCS#12 store. TlsPkcs12, /// A Kerberos keytab. Kerberos, } #[derive(Clone)] pub enum SecretOperatorVolumeScope { Node, Pod, Service { name: String }, ListenerVolume { name: String }, } /// Reference to a listener class or listener name #[derive(Clone, Debug, Eq, PartialEq)] pub enum ListenerReference { ListenerClass(String), ListenerName(String), } impl ListenerReference { /// Return the key and value for a Kubernetes object annotation fn to_annotation(&self) -> Result<Annotation, AnnotationError> { match self { ListenerReference::ListenerClass(class) => { Annotation::try_from(("listeners.stackable.tech/listener-class", class.as_str())) } ListenerReference::ListenerName(name) => { Annotation::try_from(("listeners.stackable.tech/listener-name", name.as_str())) } } } } // NOTE (Techassi): We might want to think about these names and how long they // are getting. #[derive(Debug, PartialEq, Snafu)] pub enum ListenerOperatorVolumeSourceBuilderError { #[snafu(display("failed to convert listener reference into Kubernetes annotation"))] ListenerReferenceAnnotation { source: AnnotationError }, #[snafu(display("invalid recommended labels"))] RecommendedLabels { source: LabelError }, } /// Builder for an [`EphemeralVolumeSource`] containing the listener configuration /// /// # Example /// /// ``` /// # use k8s_openapi::api::core::v1::Volume; /// # use stackable_operator::builder::pod::volume::ListenerReference; /// # use stackable_operator::builder::pod::volume::ListenerOperatorVolumeSourceBuilder; /// # use stackable_operator::builder::pod::PodBuilder; /// # use stackable_operator::kvp::Labels; /// # use k8s_openapi::{ /// # apimachinery::pkg::apis::meta::v1::ObjectMeta, /// # }; /// # use std::collections::BTreeMap; /// let mut pod_builder = PodBuilder::new(); /// /// let labels: Labels = Labels::new(); /// /// let volume_source = /// ListenerOperatorVolumeSourceBuilder::new( /// &ListenerReference::ListenerClass("nodeport".into()), /// &labels, /// ) /// .unwrap() /// .build_ephemeral() /// .unwrap(); /// /// pod_builder /// .add_volume(Volume { /// name: "listener".to_string(), /// ephemeral: Some(volume_source), /// ..Volume::default() /// }); /// /// // There is also a shortcut for the code above: /// pod_builder /// .add_listener_volume_by_listener_class("listener", "nodeport", &labels); /// ``` #[derive(Clone, Debug)] pub struct ListenerOperatorVolumeSourceBuilder { listener_reference: ListenerReference, labels: Labels, } impl ListenerOperatorVolumeSourceBuilder { /// Create a builder for the given listener class or listener name pub fn new( listener_reference: &ListenerReference, labels: &Labels, ) -> Result<ListenerOperatorVolumeSourceBuilder, ListenerOperatorVolumeSourceBuilderError> { Ok(Self { listener_reference: listener_reference.to_owned(), labels: labels.to_owned(), }) } fn build_spec(&self) -> PersistentVolumeClaimSpec { PersistentVolumeClaimSpec { storage_class_name: Some("listeners.stackable.tech".to_string()), resources: Some(VolumeResourceRequirements { requests: Some([("storage".to_string(), Quantity("1".to_string()))].into()), ..Default::default() }), access_modes: Some(vec!["ReadWriteMany".to_string()]), ..PersistentVolumeClaimSpec::default() } } #[deprecated(note = "renamed to `build_ephemeral`", since = "0.61.1")] pub fn build(&self) -> Result<EphemeralVolumeSource, ListenerOperatorVolumeSourceBuilderError> { self.build_ephemeral() } /// Build an [`EphemeralVolumeSource`] from the builder. pub fn build_ephemeral( &self, ) -> Result<EphemeralVolumeSource, ListenerOperatorVolumeSourceBuilderError> { let listener_reference_annotation = self .listener_reference .to_annotation() .context(ListenerReferenceAnnotationSnafu)?; Ok(EphemeralVolumeSource { volume_claim_template: Some(PersistentVolumeClaimTemplate { metadata: Some( ObjectMetaBuilder::new() .with_annotation(listener_reference_annotation) .with_labels(self.labels.clone()) .build(), ), spec: self.build_spec(), }), }) } /// Build a [`PersistentVolumeClaim`] from the builder. pub fn build_pvc( &self, name: impl Into<String>, ) -> Result<PersistentVolumeClaim, ListenerOperatorVolumeSourceBuilderError> { let listener_reference_annotation = self .listener_reference .to_annotation() .context(ListenerReferenceAnnotationSnafu)?; Ok(PersistentVolumeClaim { metadata: ObjectMetaBuilder::new() .name(name) .with_annotation(listener_reference_annotation) .with_labels(self.labels.clone()) .build(), spec: Some(self.build_spec()), ..Default::default() }) } } #[cfg(test)] mod tests { use super::*; use k8s_openapi::apimachinery::pkg::api::resource::Quantity; #[test] fn builder() { let mut volume_builder = VolumeBuilder::new("name"); volume_builder.with_config_map("configmap"); let vol = volume_builder.build(); assert_eq!(vol.name, "name".to_string()); assert_eq!( vol.config_map.map(|cm| cm.name), Some("configmap".to_string()) ); volume_builder.with_empty_dir(Some("medium"), Some(Quantity("quantity".to_string()))); let vol = volume_builder.build(); assert_eq!( vol.empty_dir.and_then(|dir| dir.medium), Some("medium".to_string()) ); volume_builder.with_host_path("path", Some("type_")); let vol = volume_builder.build(); assert_eq!( vol.host_path.map(|host| host.path), Some("path".to_string()) ); volume_builder.with_secret("secret", false); let vol = volume_builder.build(); assert_eq!( vol.secret.and_then(|secret| secret.secret_name), Some("secret".to_string()) ); } #[test] fn mount_builder() { let mut volume_mount_builder = VolumeMountBuilder::new("name", "mount_path"); volume_mount_builder .mount_propagation("mount_propagation") .read_only(true) .sub_path("sub_path") .sub_path_expr("sub_path_expr"); let vm = volume_mount_builder.build(); assert_eq!(vm.name, "name".to_string()); assert_eq!(vm.mount_path, "mount_path".to_string()); assert_eq!(vm.mount_propagation, Some("mount_propagation".to_string())); assert_eq!(vm.read_only, Some(true)); assert_eq!(vm.sub_path, Some("sub_path".to_string())); assert_eq!(vm.sub_path_expr, Some("sub_path_expr".to_string())); } #[test] fn listener_operator_volume_source_builder() { let labels: Labels = Labels::new(); let builder = ListenerOperatorVolumeSourceBuilder::new( &ListenerReference::ListenerClass("public".into()), &labels, ) .unwrap(); let volume_source = builder.build_ephemeral().unwrap(); let volume_claim_template = volume_source.volume_claim_template; let annotations = volume_claim_template .as_ref() .and_then(|template| template.metadata.as_ref()) .and_then(|metadata| metadata.annotations.as_ref()) .cloned() .unwrap_or_default(); let spec = volume_claim_template.unwrap_or_default().spec; let access_modes = spec.access_modes.unwrap_or_default(); let requests = spec .resources .and_then(|resources| resources.requests) .unwrap_or_default(); assert_eq!(1, annotations.len()); assert_eq!( Some(( &"listeners.stackable.tech/listener-class".to_string(), &"public".to_string() )), annotations.iter().next() ); assert_eq!( Some("listeners.stackable.tech".to_string()), spec.storage_class_name ); assert_eq!(1, access_modes.len()); assert_eq!(Some(&"ReadWriteMany".to_string()), access_modes.first()); assert_eq!(1, requests.len()); assert_eq!( Some((&"storage".to_string(), &Quantity("1".into()))), requests.iter().next() ); } }