Skip to content

Commit c9b3f2e

Browse files
committed
feat(webhook): Add gracefull shutdown
1 parent 99ceb14 commit c9b3f2e

2 files changed

Lines changed: 25 additions & 54 deletions

File tree

crates/stackable-webhook/src/lib.rs

Lines changed: 21 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,11 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1515

1616
use ::x509_cert::Certificate;
1717
use axum::{Router, routing::get};
18-
use futures_util::{FutureExt as _, TryFutureExt, select};
18+
use futures_util::TryFutureExt;
1919
use k8s_openapi::ByteString;
2020
use snafu::{ResultExt, Snafu};
2121
use stackable_telemetry::AxumTraceLayer;
22-
use tokio::{
23-
signal::unix::{SignalKind, signal},
24-
sync::mpsc,
25-
try_join,
26-
};
22+
use tokio::{sync::mpsc, try_join};
2723
use tower::ServiceBuilder;
2824
use webhooks::{Webhook, WebhookError};
2925
use x509_cert::der::{EncodePem, pem::LineEnding};
@@ -59,6 +55,7 @@ pub enum WebhookServerError {
5955
///
6056
/// ```
6157
/// use stackable_webhook::{WebhookServer, WebhookServerOptions, webhooks::Webhook};
58+
/// use tokio::time::{Duration, sleep};
6259
///
6360
/// # async fn docs() {
6461
/// let mut webhooks: Vec<Box<dyn Webhook>> = vec![];
@@ -69,8 +66,9 @@ pub enum WebhookServerError {
6966
/// webhook_service_name: "my-operator".to_owned(),
7067
/// };
7168
/// let webhook_server = WebhookServer::new(webhooks, webhook_options).await.unwrap();
69+
/// let shutdown_signal = sleep(Duration::from_millis(100));
7270
///
73-
/// webhook_server.run().await.unwrap();
71+
/// webhook_server.run(shutdown_signal).await.unwrap();
7472
/// # }
7573
/// ```
7674
pub struct WebhookServer {
@@ -154,52 +152,16 @@ impl WebhookServer {
154152
})
155153
}
156154

157-
/// Runs the Webhook server and sets up signal handlers for shutting down.
155+
/// Runs the [`WebhookServer`] and handles underlying certificate rotations of the [`TlsServer`].
158156
///
159-
/// This does not implement graceful shutdown of the underlying server. Additionally, the server
160-
/// is never started in cases where no [`Webhook`] is registered. Callers of this function need
161-
/// to ensure to choose the correct joining mechanism for their use-case to for example avoid
162-
/// unexpected shutdowns of the whole Kubernetes controller.
163-
pub async fn run(self) -> Result<()> {
164-
let future_server = self.run_server();
165-
let future_signal = async {
166-
let mut sigint = signal(SignalKind::interrupt()).expect("create SIGINT listener");
167-
let mut sigterm = signal(SignalKind::terminate()).expect("create SIGTERM listener");
168-
169-
tracing::debug!("created unix signal handlers");
170-
171-
select! {
172-
signal = sigint.recv().fuse() => {
173-
if signal.is_some() {
174-
tracing::debug!( "received SIGINT");
175-
}
176-
},
177-
signal = sigterm.recv().fuse() => {
178-
if signal.is_some() {
179-
tracing::debug!( "received SIGTERM");
180-
}
181-
},
182-
};
183-
};
184-
185-
// select requires Future + Unpin
186-
tokio::pin!(future_server);
187-
tokio::pin!(future_signal);
188-
189-
tokio::select! {
190-
res = &mut future_server => {
191-
// If the server future errors, propagate the error
192-
res?;
193-
}
194-
_ = &mut future_signal => {
195-
tracing::info!("shutdown signal received, stopping webhook server");
196-
}
197-
}
198-
199-
Ok(())
200-
}
201-
202-
async fn run_server(self) -> Result<()> {
157+
/// It should be noted that the server is never started in cases where no [`Webhook`] is
158+
/// registered. Callers of this function need to ensure to choose the correct joining mechanism
159+
/// for their use-case to for example avoid unexpected shutdowns of the whole Kubernetes
160+
/// controller.
161+
pub async fn run<F>(self, shutdown_signal: F) -> Result<()>
162+
where
163+
F: Future<Output = ()>,
164+
{
203165
tracing::debug!("run webhook server");
204166

205167
let Self {
@@ -217,10 +179,14 @@ impl WebhookServer {
217179
}
218180

219181
let tls_server = tls_server
220-
.run()
182+
.run(shutdown_signal)
221183
.map_err(|err| WebhookServerError::RunTlsServer { source: err });
222184

223185
let cert_update_loop = async {
186+
// Once the shutdown signal is triggered, the TlsServer above should be dropped as the
187+
// run associated function consumes self. This in turn means that when the receiver is
188+
// polled, it will return `Ok(Ready(None))`, which will cause this while loop to break
189+
// and the future to complete.
224190
while let Some(cert) = cert_rx.recv().await {
225191
// The caBundle needs to be provided as a base64-encoded PEM envelope.
226192
let ca_bundle = cert
@@ -243,6 +209,8 @@ impl WebhookServer {
243209
Ok(())
244210
};
245211

212+
// This either returns if one of the two futures complete with Err(_) or when both complete
213+
// with Ok(_). Both futures complete with Ok(_) when a shutdown signal is received.
246214
try_join!(cert_update_loop, tls_server).map(|_| ())
247215
}
248216
}

crates/stackable-webhook/src/webhooks/conversion_webhook.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub enum ConversionWebhookError {
5151
/// WebhookServer,
5252
/// webhooks::{ConversionWebhook, ConversionWebhookOptions},
5353
/// };
54+
/// use tokio::time::{Duration, sleep};
5455
///
5556
/// # async fn docs() {
5657
/// // The Kubernetes client
@@ -75,7 +76,9 @@ pub enum ConversionWebhookError {
7576
/// let webhook_server = WebhookServer::new(vec![Box::new(conversion_webhook)], webhook_options)
7677
/// .await
7778
/// .unwrap();
78-
/// webhook_server.run().await.unwrap();
79+
/// let shutdown_signal = sleep(Duration::from_millis(100));
80+
///
81+
/// webhook_server.run(shutdown_signal).await.unwrap();
7982
/// # }
8083
/// ```
8184
pub struct ConversionWebhook<H> {

0 commit comments

Comments
 (0)