diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d248fb94..ae3661fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Builder for `EphemeralVolumeSource`s added which are used by the listener-operator ([#496]). + +[#496]: https://github.com/stackabletech/operator-rs/pull/496 + ## [0.26.0] - 2022-10-20 ### Added diff --git a/src/builder/pod/mod.rs b/src/builder/pod/mod.rs index ab4687251..34d338057 100644 --- a/src/builder/pod/mod.rs +++ b/src/builder/pod/mod.rs @@ -15,6 +15,8 @@ use k8s_openapi::{ }; use std::collections::BTreeMap; +use super::{ListenerOperatorVolumeSourceBuilder, ListenerReference}; + /// A builder to build [`Pod`] objects. /// #[derive(Clone, Default)] @@ -146,6 +148,138 @@ impl PodBuilder { self } + /// Add a [`Volume`] for the storage class `listeners.stackable.tech` with the given listener + /// class. + /// + /// # Example + /// + /// ``` + /// # use stackable_operator::builder::PodBuilder; + /// # use stackable_operator::builder::ContainerBuilder; + /// let pod = PodBuilder::new() + /// .metadata_default() + /// .add_container( + /// ContainerBuilder::new("container") + /// .unwrap() + /// .add_volume_mount("listener", "/path/to/volume") + /// .build(), + /// ) + /// .add_listener_volume_by_listener_class("listener", "nodeport") + /// .build() + /// .unwrap(); + /// + /// assert_eq!("\ + /// apiVersion: v1 + /// kind: Pod + /// metadata: {} + /// spec: + /// affinity: {} + /// containers: + /// - name: container + /// volumeMounts: + /// - mountPath: /path/to/volume + /// name: listener + /// enableServiceLinks: false + /// volumes: + /// - ephemeral: + /// volumeClaimTemplate: + /// metadata: + /// annotations: + /// listeners.stackable.tech/listener-class: nodeport + /// spec: + /// accessModes: + /// - ReadWriteMany + /// resources: + /// requests: + /// storage: '1' + /// storageClassName: listeners.stackable.tech + /// name: listener + /// ", serde_yaml::to_string(&pod).unwrap()) + /// ``` + pub fn add_listener_volume_by_listener_class( + &mut self, + volume_name: &str, + listener_class: &str, + ) -> &mut Self { + self.add_volume(Volume { + name: volume_name.into(), + ephemeral: Some( + ListenerOperatorVolumeSourceBuilder::new(&ListenerReference::ListenerClass( + listener_class.into(), + )) + .build(), + ), + ..Volume::default() + }); + self + } + + /// Add a [`Volume`] for the storage class `listeners.stackable.tech` with the given listener + /// name. + /// + /// # Example + /// + /// ``` + /// # use stackable_operator::builder::PodBuilder; + /// # use stackable_operator::builder::ContainerBuilder; + /// let pod = PodBuilder::new() + /// .metadata_default() + /// .add_container( + /// ContainerBuilder::new("container") + /// .unwrap() + /// .add_volume_mount("listener", "/path/to/volume") + /// .build(), + /// ) + /// .add_listener_volume_by_listener_name("listener", "preprovisioned-listener") + /// .build() + /// .unwrap(); + /// + /// assert_eq!("\ + /// apiVersion: v1 + /// kind: Pod + /// metadata: {} + /// spec: + /// affinity: {} + /// containers: + /// - name: container + /// volumeMounts: + /// - mountPath: /path/to/volume + /// name: listener + /// enableServiceLinks: false + /// volumes: + /// - ephemeral: + /// volumeClaimTemplate: + /// metadata: + /// annotations: + /// listeners.stackable.tech/listener-name: preprovisioned-listener + /// spec: + /// accessModes: + /// - ReadWriteMany + /// resources: + /// requests: + /// storage: '1' + /// storageClassName: listeners.stackable.tech + /// name: listener + /// ", serde_yaml::to_string(&pod).unwrap()) + /// ``` + pub fn add_listener_volume_by_listener_name( + &mut self, + volume_name: &str, + listener_name: &str, + ) -> &mut Self { + self.add_volume(Volume { + name: volume_name.into(), + ephemeral: Some( + ListenerOperatorVolumeSourceBuilder::new(&ListenerReference::ListenerName( + listener_name.into(), + )) + .build(), + ), + ..Volume::default() + }); + self + } + pub fn image_pull_secrets( &mut self, secrets: impl IntoIterator + Iterator, diff --git a/src/builder/pod/volume.rs b/src/builder/pod/volume.rs index 458798973..7747b1f86 100644 --- a/src/builder/pod/volume.rs +++ b/src/builder/pod/volume.rs @@ -339,6 +339,91 @@ enum SecretOperatorVolumeScope { Service { 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) -> (String, String) { + match self { + ListenerReference::ListenerClass(value) => ( + "listeners.stackable.tech/listener-class".into(), + value.into(), + ), + ListenerReference::ListenerName(value) => ( + "listeners.stackable.tech/listener-name".into(), + value.into(), + ), + } + } +} + +/// Builder for an [`EphemeralVolumeSource`] containing the listener configuration +/// +/// # Example +/// +/// ``` +/// # use k8s_openapi::api::core::v1::Volume; +/// # use stackable_operator::builder::ListenerReference; +/// # use stackable_operator::builder::ListenerOperatorVolumeSourceBuilder; +/// # use stackable_operator::builder::PodBuilder; +/// let mut pod_builder = PodBuilder::new(); +/// +/// let volume_source = ListenerOperatorVolumeSourceBuilder::new( +/// &ListenerReference::ListenerClass("nodeport".into()), +/// ) +/// .build(); +/// 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"); +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ListenerOperatorVolumeSourceBuilder { + listener_reference: ListenerReference, +} + +impl ListenerOperatorVolumeSourceBuilder { + /// Create a builder for the given listener class or listener name + pub fn new(listener_reference: &ListenerReference) -> Self { + Self { + listener_reference: listener_reference.to_owned(), + } + } + + /// Build an [`EphemeralVolumeSource`] from the builder + pub fn build(&self) -> EphemeralVolumeSource { + EphemeralVolumeSource { + volume_claim_template: Some(PersistentVolumeClaimTemplate { + metadata: Some( + ObjectMetaBuilder::new() + .annotations([self.listener_reference.to_annotation()].into()) + .build(), + ), + spec: PersistentVolumeClaimSpec { + storage_class_name: Some("listeners.stackable.tech".to_string()), + resources: Some(ResourceRequirements { + requests: Some([("storage".to_string(), Quantity("1".to_string()))].into()), + ..ResourceRequirements::default() + }), + access_modes: Some(vec!["ReadWriteMany".to_string()]), + ..PersistentVolumeClaimSpec::default() + }, + }), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -399,4 +484,47 @@ mod tests { assert_eq!(vm.sub_path, Some("sub_path".to_string())); assert_eq!(vm.sub_path_expr, Some("sub_path_expr".to_string())); } + + #[test] + fn test_listener_operator_volume_source_builder() { + let builder = ListenerOperatorVolumeSourceBuilder::new(&ListenerReference::ListenerClass( + "public".into(), + )); + + let volume_source = builder.build(); + + 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() + ); + } }