From 08b25fac91398f144e8d09007c9b4bb822484610 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 13 Aug 2025 17:18:31 +0200 Subject: [PATCH 01/10] WIP --- .../stackable-operator/src/builder/pod/mod.rs | 1 + .../src/builder/pod/probe.rs | 259 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 crates/stackable-operator/src/builder/pod/probe.rs diff --git a/crates/stackable-operator/src/builder/pod/mod.rs b/crates/stackable-operator/src/builder/pod/mod.rs index 7cb46f185..c5cb895f5 100644 --- a/crates/stackable-operator/src/builder/pod/mod.rs +++ b/crates/stackable-operator/src/builder/pod/mod.rs @@ -29,6 +29,7 @@ use crate::{ }; pub mod container; +pub mod probe; pub mod resources; pub mod security; pub mod volume; diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs new file mode 100644 index 000000000..e8d02b5f9 --- /dev/null +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -0,0 +1,259 @@ +use k8s_openapi::{ + api::core::v1::{ExecAction, GRPCAction, HTTPGetAction, Probe, TCPSocketAction}, + apimachinery::pkg::util::intstr::IntOrString, +}; + +use crate::time::Duration; + +#[derive(Debug)] +pub struct ProbeBuilder { + action: Action, + period: Period, + + success_threshold: i32, + failure_threshold: i32, + timeout: Duration, + initial_delay: Duration, + termination_grace_period: Duration, +} + +impl Default for ProbeBuilder<(), ()> { + fn default() -> Self { + Self { + action: (), + period: (), + // The following values match the Kubernetes default + success_threshold: 1, + failure_threshold: 1, + timeout: Duration::from_secs(1), + initial_delay: Duration::from_secs(0), + termination_grace_period: Duration::from_secs(0), + } + } +} + +pub enum ProbeAction { + Exec(ExecAction), + Grpc(GRPCAction), + HttpGet(HTTPGetAction), + TcpSocket(TCPSocketAction), +} + +impl ProbeBuilder<(), Period> { + /// This probe action executes the specified command + pub fn with_exec_action_helper( + self, + command: impl IntoIterator>, + ) -> ProbeBuilder { + self.with_exec_action(ExecAction { + command: Some(command.into_iter().map(Into::into).collect()), + }) + } + + /// There is a convenience helper: [`Self::with_exec_action_helper`]. + pub fn with_exec_action(self, exec_action: ExecAction) -> ProbeBuilder { + self.with_action(ProbeAction::Exec(exec_action)) + } + + pub fn with_grpc_action(self, grpc_action: GRPCAction) -> ProbeBuilder { + self.with_action(ProbeAction::Grpc(grpc_action)) + } + + /// This probe action does an HTTP GET request to the specified port. Optionally, you can + /// configure the path, otherwise the Kubernetes default is used. + pub fn with_http_get_action_helper( + self, + port: u16, + path: Option>, + ) -> ProbeBuilder { + self.with_http_get_action(HTTPGetAction { + path: path.map(Into::into), + port: IntOrString::Int(port.into()), + ..Default::default() + }) + } + + /// There is a convenience helper: [`Self::with_http_get_action_helper`]. + pub fn with_http_get_action( + self, + http_get_action: HTTPGetAction, + ) -> ProbeBuilder { + self.with_action(ProbeAction::HttpGet(http_get_action)) + } + + pub fn with_tcp_socket_action( + self, + tcp_socket_action: TCPSocketAction, + ) -> ProbeBuilder { + self.with_action(ProbeAction::TcpSocket(tcp_socket_action)) + } + + /// Action-specific functions (e.g. [`Self::with_exec_action`] or [`Self::with_http_get_action`]) + /// are recommended instead. + pub fn with_action(self, action: ProbeAction) -> ProbeBuilder { + let Self { + action: (), + period, + success_threshold, + failure_threshold, + timeout, + initial_delay, + termination_grace_period, + } = self; + + ProbeBuilder { + action, + period, + success_threshold, + failure_threshold, + timeout, + initial_delay, + termination_grace_period, + } + } +} + +impl ProbeBuilder { + /// The period/interval in which the probe should be executed. + pub fn with_period(self, period: Duration) -> ProbeBuilder { + let Self { + action, + period: (), + success_threshold, + failure_threshold, + timeout, + initial_delay, + termination_grace_period, + } = self; + + ProbeBuilder { + action, + period, + success_threshold, + failure_threshold, + timeout, + initial_delay, + termination_grace_period, + } + } +} + +// success_threshold: i32, +// failure_threshold: i32, +// timeout: Duration, +// initial_delay: Duration, +// termination_grace_period: Duration, + +impl ProbeBuilder { + /// How often the probe must succeed before being considered successful. + pub fn with_success_threshold(mut self, success_threshold: i32) -> Self { + self.success_threshold = success_threshold; + self + } + + /// The duration the probe needs to succeed before being considered successful. + /// + /// This internally calculates the needed success threshold based on the period and passes that + /// to [`Self::with_success_threshold`]. + pub fn with_success_threshold_duration(self, success_threshold_duration: Duration) -> Self { + let success_threshold = success_threshold_duration.div_duration_f32(*self.period); + // SAFETY: Returning an Result here would hurt the builder ergonomics and having such big + // numbers does not have any real world effect. + let success_threshold = success_threshold.ceil() as i32; + self.with_success_threshold(success_threshold) + } + + /// How often the probe must fail before being considered failed. + pub fn with_failure_threshold(mut self, failure_threshold: i32) -> Self { + self.failure_threshold = failure_threshold; + self + } + + /// The duration the probe needs to fail before being considered failed. + /// + /// This internally calculates the needed failure threshold based on the period and passes that + /// to [`Self::with_failure_threshold`]. + pub fn with_failure_threshold_duration(self, failure_threshold_duration: Duration) -> Self { + let failure_threshold = failure_threshold_duration.div_duration_f32(*self.period); + // SAFETY: Returning an Result here would hurt the builder ergonomics and having such big + // numbers does not have any real world effect. + let failure_threshold = failure_threshold.ceil() as i32; + self.with_failure_threshold(failure_threshold) + } + + pub fn build(self) -> Probe { + let mut probe = Probe { + exec: None, + failure_threshold: Some(self.failure_threshold), + grpc: None, + http_get: None, + initial_delay_seconds: Some( + self.initial_delay + .as_secs() + .try_into() + .expect("TODO Error handling"), + ), + period_seconds: Some( + self.period + .as_secs() + .try_into() + .expect("TODO Error handling"), + ), + success_threshold: Some(self.success_threshold), + tcp_socket: None, + termination_grace_period_seconds: Some( + self.termination_grace_period + .as_secs() + .try_into() + .expect("TODO Error handling"), + ), + timeout_seconds: Some( + self.timeout + .as_secs() + .try_into() + .expect("TODO Error handling"), + ), + }; + + match self.action { + ProbeAction::Exec(exec_action) => probe.exec = Some(exec_action), + ProbeAction::Grpc(grpc_action) => probe.grpc = Some(grpc_action), + ProbeAction::HttpGet(http_get_action) => probe.http_get = Some(http_get_action), + ProbeAction::TcpSocket(tcp_socket_action) => probe.tcp_socket = Some(tcp_socket_action), + } + + probe + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_probe_builder() { + let probe = ProbeBuilder::default() + .with_exec_action_helper(["sleep", "1"]) + .with_period(Duration::from_secs(5)) + .with_failure_threshold_duration(Duration::from_secs(33)) + .build(); + + assert_eq!( + probe, + Probe { + exec: Some(ExecAction { + command: Some(vec!["sleep".to_owned(), "1".to_owned()]) + }), + failure_threshold: Some(7), + grpc: None, + http_get: None, + initial_delay_seconds: Some(0), + period_seconds: Some(5), + success_threshold: Some(1), + tcp_socket: None, + termination_grace_period_seconds: Some(0), + timeout_seconds: Some(1), + } + ); + } +} From 627638712ef895c7348a504b599904d8295c2d89 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Wed, 13 Aug 2025 17:25:47 +0200 Subject: [PATCH 02/10] Improve tests --- .../src/builder/pod/probe.rs | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index e8d02b5f9..2b7a007fa 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -7,9 +7,11 @@ use crate::time::Duration; #[derive(Debug)] pub struct ProbeBuilder { + // Mandatory field action: Action, period: Period, + // Fields with defaults success_threshold: i32, failure_threshold: i32, timeout: Duration, @@ -22,7 +24,7 @@ impl Default for ProbeBuilder<(), ()> { Self { action: (), period: (), - // The following values match the Kubernetes default + // The following values match the Kubernetes defaults success_threshold: 1, failure_threshold: 1, timeout: Duration::from_secs(1), @@ -64,10 +66,10 @@ impl ProbeBuilder<(), Period> { pub fn with_http_get_action_helper( self, port: u16, - path: Option>, + path: Option, ) -> ProbeBuilder { self.with_http_get_action(HTTPGetAction { - path: path.map(Into::into), + path, port: IntOrString::Int(port.into()), ..Default::default() }) @@ -138,12 +140,6 @@ impl ProbeBuilder { } } -// success_threshold: i32, -// failure_threshold: i32, -// timeout: Duration, -// initial_delay: Duration, -// termination_grace_period: Duration, - impl ProbeBuilder { /// How often the probe must succeed before being considered successful. pub fn with_success_threshold(mut self, success_threshold: i32) -> Self { @@ -169,6 +165,21 @@ impl ProbeBuilder { self } + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + pub fn with_initial_delay(mut self, initial_delay: Duration) -> Self { + self.initial_delay = initial_delay; + self + } + + pub fn with_termination_grace_period(mut self, termination_grace_period: Duration) -> Self { + self.termination_grace_period = termination_grace_period; + self + } + /// The duration the probe needs to fail before being considered failed. /// /// This internally calculates the needed failure threshold based on the period and passes that @@ -231,11 +242,32 @@ mod tests { use super::*; #[test] - fn test_probe_builder() { + fn test_probe_builder_minimal() { + let probe = ProbeBuilder::default() + .with_http_get_action_helper(8080, None) + .with_period(Duration::from_secs(10)) + .build(); + + assert_eq!( + probe.http_get, + Some(HTTPGetAction { + port: IntOrString::Int(8080), + ..Default::default() + }) + ); + assert_eq!(probe.period_seconds, Some(10)); + } + + #[test] + fn test_probe_builder_complex() { let probe = ProbeBuilder::default() .with_exec_action_helper(["sleep", "1"]) .with_period(Duration::from_secs(5)) + .with_success_threshold(2) .with_failure_threshold_duration(Duration::from_secs(33)) + .with_timeout(Duration::from_secs(3)) + .with_initial_delay(Duration::from_secs(7)) + .with_termination_grace_period(Duration::from_secs(4)) .build(); assert_eq!( @@ -247,12 +279,12 @@ mod tests { failure_threshold: Some(7), grpc: None, http_get: None, - initial_delay_seconds: Some(0), + initial_delay_seconds: Some(7), period_seconds: Some(5), - success_threshold: Some(1), + success_threshold: Some(2), tcp_socket: None, - termination_grace_period_seconds: Some(0), - timeout_seconds: Some(1), + termination_grace_period_seconds: Some(4), + timeout_seconds: Some(3), } ); } From df4abc051465d483832cb57d3a8ae8bfe34e7b91 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 15 Aug 2025 10:28:18 +0200 Subject: [PATCH 03/10] refactor according to review --- .../src/builder/pod/probe.rs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index 2b7a007fa..8d57db6fe 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -41,35 +41,39 @@ pub enum ProbeAction { TcpSocket(TCPSocketAction), } -impl ProbeBuilder<(), Period> { +impl ProbeBuilder<(), ()> { /// This probe action executes the specified command pub fn with_exec_action_helper( self, command: impl IntoIterator>, - ) -> ProbeBuilder { + ) -> ProbeBuilder { self.with_exec_action(ExecAction { command: Some(command.into_iter().map(Into::into).collect()), }) } /// There is a convenience helper: [`Self::with_exec_action_helper`]. - pub fn with_exec_action(self, exec_action: ExecAction) -> ProbeBuilder { + pub fn with_exec_action(self, exec_action: ExecAction) -> ProbeBuilder { self.with_action(ProbeAction::Exec(exec_action)) } - pub fn with_grpc_action(self, grpc_action: GRPCAction) -> ProbeBuilder { + pub fn with_grpc_action(self, grpc_action: GRPCAction) -> ProbeBuilder { self.with_action(ProbeAction::Grpc(grpc_action)) } + // Note: Ideally we also have a builder for `HTTPGetAction`, but that is lot's of effort we + // don't want to spend now. /// This probe action does an HTTP GET request to the specified port. Optionally, you can /// configure the path, otherwise the Kubernetes default is used. pub fn with_http_get_action_helper( self, port: u16, + scheme: Option, path: Option, - ) -> ProbeBuilder { + ) -> ProbeBuilder { self.with_http_get_action(HTTPGetAction { path, + scheme, port: IntOrString::Int(port.into()), ..Default::default() }) @@ -79,20 +83,20 @@ impl ProbeBuilder<(), Period> { pub fn with_http_get_action( self, http_get_action: HTTPGetAction, - ) -> ProbeBuilder { + ) -> ProbeBuilder { self.with_action(ProbeAction::HttpGet(http_get_action)) } pub fn with_tcp_socket_action( self, tcp_socket_action: TCPSocketAction, - ) -> ProbeBuilder { + ) -> ProbeBuilder { self.with_action(ProbeAction::TcpSocket(tcp_socket_action)) } /// Action-specific functions (e.g. [`Self::with_exec_action`] or [`Self::with_http_get_action`]) /// are recommended instead. - pub fn with_action(self, action: ProbeAction) -> ProbeBuilder { + pub fn with_action(self, action: ProbeAction) -> ProbeBuilder { let Self { action: (), period, @@ -244,7 +248,7 @@ mod tests { #[test] fn test_probe_builder_minimal() { let probe = ProbeBuilder::default() - .with_http_get_action_helper(8080, None) + .with_http_get_action_helper(8080, None, None) .with_period(Duration::from_secs(10)) .build(); From 18026bf32fdea5438b6ff817a4c5bb69edfc6fa4 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 15 Aug 2025 11:58:05 +0200 Subject: [PATCH 04/10] Add error handling --- .../src/builder/pod/probe.rs | 109 ++++++++++++------ 1 file changed, 72 insertions(+), 37 deletions(-) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index 8d57db6fe..5d96d3654 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -1,10 +1,28 @@ +use std::{i32, num::TryFromIntError}; + use k8s_openapi::{ api::core::v1::{ExecAction, GRPCAction, HTTPGetAction, Probe, TCPSocketAction}, apimachinery::pkg::util::intstr::IntOrString, }; +use snafu::{ResultExt, Snafu, ensure}; use crate::time::Duration; +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display( + "The probe's {field:?} duration of {duration} is too long, as it's seconds doesn't fit into an i32" + ))] + DurationTooLong { + source: TryFromIntError, + field: String, + duration: Duration, + }, + + #[snafu(display("The probe period is zero, but it needs to be a positive duration"))] + PeriodIsZero {}, +} + #[derive(Debug)] pub struct ProbeBuilder { // Mandatory field @@ -153,14 +171,20 @@ impl ProbeBuilder { /// The duration the probe needs to succeed before being considered successful. /// - /// This internally calculates the needed success threshold based on the period and passes that + /// This internally calculates the needed failure threshold based on the period and passes that /// to [`Self::with_success_threshold`]. - pub fn with_success_threshold_duration(self, success_threshold_duration: Duration) -> Self { + /// + /// This function returns an [`Error::PeriodIsZero`] error in case the period is zero, as it + /// can not divide by zero. + pub fn with_success_threshold_duration( + self, + success_threshold_duration: Duration, + ) -> Result { + ensure!(self.period.as_nanos() != 0, PeriodIsZeroSnafu); + + // SAFETY: Period is checked above to be non-zero let success_threshold = success_threshold_duration.div_duration_f32(*self.period); - // SAFETY: Returning an Result here would hurt the builder ergonomics and having such big - // numbers does not have any real world effect. - let success_threshold = success_threshold.ceil() as i32; - self.with_success_threshold(success_threshold) + Ok(self.with_success_threshold(success_threshold.ceil() as i32)) } /// How often the probe must fail before being considered failed. @@ -188,46 +212,54 @@ impl ProbeBuilder { /// /// This internally calculates the needed failure threshold based on the period and passes that /// to [`Self::with_failure_threshold`]. - pub fn with_failure_threshold_duration(self, failure_threshold_duration: Duration) -> Self { + /// + /// This function returns an [`Error::PeriodIsZero`] error in case the period is zero, as it + /// can not divide by zero. + pub fn with_failure_threshold_duration( + self, + failure_threshold_duration: Duration, + ) -> Result { + ensure!(self.period.as_nanos() != 0, PeriodIsZeroSnafu); + + // SAFETY: Period is checked above to be non-zero let failure_threshold = failure_threshold_duration.div_duration_f32(*self.period); - // SAFETY: Returning an Result here would hurt the builder ergonomics and having such big - // numbers does not have any real world effect. - let failure_threshold = failure_threshold.ceil() as i32; - self.with_failure_threshold(failure_threshold) + Ok(self.with_failure_threshold(failure_threshold.ceil() as i32)) } - pub fn build(self) -> Probe { + pub fn build(self) -> Result { let mut probe = Probe { exec: None, failure_threshold: Some(self.failure_threshold), grpc: None, http_get: None, - initial_delay_seconds: Some( - self.initial_delay - .as_secs() - .try_into() - .expect("TODO Error handling"), - ), - period_seconds: Some( - self.period - .as_secs() - .try_into() - .expect("TODO Error handling"), - ), + initial_delay_seconds: Some(self.initial_delay.as_secs().try_into().context( + DurationTooLongSnafu { + field: "initialDelay", + duration: self.initial_delay, + }, + )?), + period_seconds: Some(self.period.as_secs().try_into().context( + DurationTooLongSnafu { + field: "period", + duration: self.period, + }, + )?), success_threshold: Some(self.success_threshold), tcp_socket: None, termination_grace_period_seconds: Some( - self.termination_grace_period - .as_secs() - .try_into() - .expect("TODO Error handling"), - ), - timeout_seconds: Some( - self.timeout - .as_secs() - .try_into() - .expect("TODO Error handling"), + self.termination_grace_period.as_secs().try_into().context( + DurationTooLongSnafu { + field: "terminationGracePeriod", + duration: self.termination_grace_period, + }, + )?, ), + timeout_seconds: Some(self.timeout.as_secs().try_into().context( + DurationTooLongSnafu { + field: "timeout", + duration: self.timeout, + }, + )?), }; match self.action { @@ -237,7 +269,7 @@ impl ProbeBuilder { ProbeAction::TcpSocket(tcp_socket_action) => probe.tcp_socket = Some(tcp_socket_action), } - probe + Ok(probe) } } @@ -250,7 +282,8 @@ mod tests { let probe = ProbeBuilder::default() .with_http_get_action_helper(8080, None, None) .with_period(Duration::from_secs(10)) - .build(); + .build() + .expect("Valid inputs must produce a Probe"); assert_eq!( probe.http_get, @@ -269,10 +302,12 @@ mod tests { .with_period(Duration::from_secs(5)) .with_success_threshold(2) .with_failure_threshold_duration(Duration::from_secs(33)) + .expect("The period is always non-zero") .with_timeout(Duration::from_secs(3)) .with_initial_delay(Duration::from_secs(7)) .with_termination_grace_period(Duration::from_secs(4)) - .build(); + .build() + .expect("Valid inputs must produce a Probe"); assert_eq!( probe, From 3b9a7792efdc604bc4ee69fdbae1cd6b6af039de Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 15 Aug 2025 11:59:16 +0200 Subject: [PATCH 05/10] changelog --- crates/stackable-operator/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 86945a87f..dbc2124cb 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add `ProbeBuilder` to build Kubernetes container probes ([#1078]). + ### Changed - BREAKING: The `ResolvedProductImage` field `app_version_label` was renamed to `app_version_label_value` to match changes to its type ([#1076]). @@ -14,6 +18,7 @@ All notable changes to this project will be documented in this file. This is the case when referencing custom images via a `@sha256:...` hash. As such, the `product_image_selection::resolve` function is now fallible ([#1076]). [#1076]: https://github.com/stackabletech/operator-rs/pull/1076 +[#1078]: https://github.com/stackabletech/operator-rs/pull/1078 ## [0.94.0] - 2025-07-10 From d40b82e26bbe063e2973ab8923ee85cb45d63c34 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Fri, 15 Aug 2025 14:14:03 +0200 Subject: [PATCH 06/10] clippy --- crates/stackable-operator/src/builder/pod/probe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index 5d96d3654..025eb4270 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -1,4 +1,4 @@ -use std::{i32, num::TryFromIntError}; +use std::num::TryFromIntError; use k8s_openapi::{ api::core::v1::{ExecAction, GRPCAction, HTTPGetAction, Probe, TCPSocketAction}, From ecc2373cfaa3c78179f0ce9963a90a3643d92278 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Mon, 18 Aug 2025 15:34:08 +0200 Subject: [PATCH 07/10] Add docs on ProbeBuilder --- .../src/builder/pod/probe.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index 025eb4270..508dce20d 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -23,6 +23,37 @@ pub enum Error { PeriodIsZero {}, } +/// Kubernetes [`Probe`] builder. +/// +/// The upstream [`Probe`] struct does not prevent invalid probe configurations +/// which leads to surprises at runtime which can be deeply hidden. +/// You need to specify at least an action and interval (in this order). +/// +/// ### Usage example +/// +/// ``` +/// use stackable_operator::{ +/// builder::pod::probe::ProbeBuilder, +/// time::Duration, +/// }; +/// # use k8s_openapi::api::core::v1::HTTPGetAction; +/// # use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; +/// +/// let probe = ProbeBuilder::default() +/// .with_http_get_action_helper(8080, None, None) +/// .with_period(Duration::from_secs(10)) +/// .build() +/// .expect("failed to build probe"); +/// +/// assert_eq!( +/// probe.http_get, +/// Some(HTTPGetAction { +/// port: IntOrString::Int(8080), +/// ..Default::default() +/// }) +/// ); +/// assert_eq!(probe.period_seconds, Some(10)); +/// ``` #[derive(Debug)] pub struct ProbeBuilder { // Mandatory field From 55f20c698e95670837727de0e820f9e9560f566f Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Mon, 18 Aug 2025 15:38:35 +0200 Subject: [PATCH 08/10] Update crates/stackable-operator/src/builder/pod/probe.rs Co-authored-by: Nick <10092581+NickLarsenNZ@users.noreply.github.com> --- crates/stackable-operator/src/builder/pod/probe.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index 508dce20d..5cb90cf9b 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -83,6 +83,12 @@ impl Default for ProbeBuilder<(), ()> { } } +/// Available probes +/// +/// Only one probe can be configured at a time. For more details about each +/// type, see [container-probes] documentation. +/// +/// [container-probes]: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes pub enum ProbeAction { Exec(ExecAction), Grpc(GRPCAction), From 7779e124437367a2fbc4dce7f0e46cdc49aaeb5c Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Mon, 18 Aug 2025 16:04:23 +0200 Subject: [PATCH 09/10] Start directly with action functions. Add docs --- .../src/builder/pod/probe.rs | 107 +++++++----------- 1 file changed, 40 insertions(+), 67 deletions(-) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index 5cb90cf9b..6ac313da9 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -39,8 +39,7 @@ pub enum Error { /// # use k8s_openapi::api::core::v1::HTTPGetAction; /// # use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; /// -/// let probe = ProbeBuilder::default() -/// .with_http_get_action_helper(8080, None, None) +/// let probe = ProbeBuilder::http_get_port_scheme_path(8080, None, None) /// .with_period(Duration::from_secs(10)) /// .build() /// .expect("failed to build probe"); @@ -68,24 +67,9 @@ pub struct ProbeBuilder { termination_grace_period: Duration, } -impl Default for ProbeBuilder<(), ()> { - fn default() -> Self { - Self { - action: (), - period: (), - // The following values match the Kubernetes defaults - success_threshold: 1, - failure_threshold: 1, - timeout: Duration::from_secs(1), - initial_delay: Duration::from_secs(0), - termination_grace_period: Duration::from_secs(0), - } - } -} - /// Available probes /// -/// Only one probe can be configured at a time. For more details about each +/// Only one probe can be configured at a time. For more details about each /// type, see [container-probes] documentation. /// /// [container-probes]: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes @@ -98,35 +82,24 @@ pub enum ProbeAction { impl ProbeBuilder<(), ()> { /// This probe action executes the specified command - pub fn with_exec_action_helper( - self, + pub fn exec_command( command: impl IntoIterator>, ) -> ProbeBuilder { - self.with_exec_action(ExecAction { + Self::exec(ExecAction { command: Some(command.into_iter().map(Into::into).collect()), }) } - /// There is a convenience helper: [`Self::with_exec_action_helper`]. - pub fn with_exec_action(self, exec_action: ExecAction) -> ProbeBuilder { - self.with_action(ProbeAction::Exec(exec_action)) - } - - pub fn with_grpc_action(self, grpc_action: GRPCAction) -> ProbeBuilder { - self.with_action(ProbeAction::Grpc(grpc_action)) - } - // Note: Ideally we also have a builder for `HTTPGetAction`, but that is lot's of effort we // don't want to spend now. /// This probe action does an HTTP GET request to the specified port. Optionally, you can - /// configure the path, otherwise the Kubernetes default is used. - pub fn with_http_get_action_helper( - self, + /// configure a scheme and path, otherwise the Kubernetes default is used. + pub fn http_get_port_scheme_path( port: u16, scheme: Option, path: Option, ) -> ProbeBuilder { - self.with_http_get_action(HTTPGetAction { + Self::http_get(HTTPGetAction { path, scheme, port: IntOrString::Int(port.into()), @@ -134,42 +107,44 @@ impl ProbeBuilder<(), ()> { }) } - /// There is a convenience helper: [`Self::with_http_get_action_helper`]. - pub fn with_http_get_action( - self, - http_get_action: HTTPGetAction, - ) -> ProbeBuilder { - self.with_action(ProbeAction::HttpGet(http_get_action)) + /// Set's an [`ExecAction`] as probe. + /// + /// You likely want to use [`Self::exec_command`] whenever possible. + pub fn exec(exec_action: ExecAction) -> ProbeBuilder { + Self::action(ProbeAction::Exec(exec_action)) } - pub fn with_tcp_socket_action( - self, - tcp_socket_action: TCPSocketAction, - ) -> ProbeBuilder { - self.with_action(ProbeAction::TcpSocket(tcp_socket_action)) + /// Set's an [`GRPCAction`] as probe. + pub fn grpc(grpc_action: GRPCAction) -> ProbeBuilder { + Self::action(ProbeAction::Grpc(grpc_action)) } - /// Action-specific functions (e.g. [`Self::with_exec_action`] or [`Self::with_http_get_action`]) - /// are recommended instead. - pub fn with_action(self, action: ProbeAction) -> ProbeBuilder { - let Self { - action: (), - period, - success_threshold, - failure_threshold, - timeout, - initial_delay, - termination_grace_period, - } = self; + /// Set's an [`HTTPGetAction`] as probe. + /// + /// For simple cases, there is a a convenience helper: [`Self::http_get_port_scheme_path`]. + pub fn http_get(http_get_action: HTTPGetAction) -> ProbeBuilder { + Self::action(ProbeAction::HttpGet(http_get_action)) + } + + /// Set's an [`TCPSocketAction`] as probe. + pub fn tcp_socket(tcp_socket_action: TCPSocketAction) -> ProbeBuilder { + Self::action(ProbeAction::TcpSocket(tcp_socket_action)) + } + /// Incase you already have an [`ProbeAction`] enum variant you can pass that here. + /// + /// If not, it is recommended to use one of the specialized functions such as [`Self::exec`], + /// [`Self::grpc`], [`Self::http_get`] or [`Self::tcp_socket`] or their helper functions. + pub fn action(action: ProbeAction) -> ProbeBuilder { ProbeBuilder { action, - period, - success_threshold, - failure_threshold, - timeout, - initial_delay, - termination_grace_period, + period: (), + // The following values match the Kubernetes defaults + success_threshold: 1, + failure_threshold: 1, + timeout: Duration::from_secs(1), + initial_delay: Duration::from_secs(0), + termination_grace_period: Duration::from_secs(0), } } } @@ -316,8 +291,7 @@ mod tests { #[test] fn test_probe_builder_minimal() { - let probe = ProbeBuilder::default() - .with_http_get_action_helper(8080, None, None) + let probe = ProbeBuilder::http_get_port_scheme_path(8080, None, None) .with_period(Duration::from_secs(10)) .build() .expect("Valid inputs must produce a Probe"); @@ -334,8 +308,7 @@ mod tests { #[test] fn test_probe_builder_complex() { - let probe = ProbeBuilder::default() - .with_exec_action_helper(["sleep", "1"]) + let probe = ProbeBuilder::exec_command(["sleep", "1"]) .with_period(Duration::from_secs(5)) .with_success_threshold(2) .with_failure_threshold_duration(Duration::from_secs(33)) From fd5a09988cdf5d9abe05b3f22c310b26d488353a Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Tue, 19 Aug 2025 15:22:47 +0200 Subject: [PATCH 10/10] Move rustdocs to module --- .../src/builder/pod/probe.rs | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/crates/stackable-operator/src/builder/pod/probe.rs b/crates/stackable-operator/src/builder/pod/probe.rs index 6ac313da9..7278dfaa2 100644 --- a/crates/stackable-operator/src/builder/pod/probe.rs +++ b/crates/stackable-operator/src/builder/pod/probe.rs @@ -1,3 +1,34 @@ +//! Kubernetes [`Probe`] builder. +//! +//! The upstream [`Probe`] struct does not prevent invalid probe configurations +//! which leads to surprises at runtime which can be deeply hidden. +//! You need to specify at least an action and interval (in this order). +//! +//! ### Usage example +//! +//! ``` +//! use stackable_operator::{ +//! builder::pod::probe::ProbeBuilder, +//! time::Duration, +//! }; +//! # use k8s_openapi::api::core::v1::HTTPGetAction; +//! # use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; +//! +//! let probe = ProbeBuilder::http_get_port_scheme_path(8080, None, None) +//! .with_period(Duration::from_secs(10)) +//! .build() +//! .expect("failed to build probe"); +//! +//! assert_eq!( +//! probe.http_get, +//! Some(HTTPGetAction { +//! port: IntOrString::Int(8080), +//! ..Default::default() +//! }) +//! ); +//! assert_eq!(probe.period_seconds, Some(10)); +//! ``` + use std::num::TryFromIntError; use k8s_openapi::{ @@ -23,36 +54,6 @@ pub enum Error { PeriodIsZero {}, } -/// Kubernetes [`Probe`] builder. -/// -/// The upstream [`Probe`] struct does not prevent invalid probe configurations -/// which leads to surprises at runtime which can be deeply hidden. -/// You need to specify at least an action and interval (in this order). -/// -/// ### Usage example -/// -/// ``` -/// use stackable_operator::{ -/// builder::pod::probe::ProbeBuilder, -/// time::Duration, -/// }; -/// # use k8s_openapi::api::core::v1::HTTPGetAction; -/// # use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; -/// -/// let probe = ProbeBuilder::http_get_port_scheme_path(8080, None, None) -/// .with_period(Duration::from_secs(10)) -/// .build() -/// .expect("failed to build probe"); -/// -/// assert_eq!( -/// probe.http_get, -/// Some(HTTPGetAction { -/// port: IntOrString::Int(8080), -/// ..Default::default() -/// }) -/// ); -/// assert_eq!(probe.period_seconds, Some(10)); -/// ``` #[derive(Debug)] pub struct ProbeBuilder { // Mandatory field