stackable_webhook/
lib.rs

1//! Utility types and functions to easily create ready-to-use webhook servers
2//! which can handle different tasks, for example CRD conversions. All webhook
3//! servers use HTTPS by default. This library is fully compatible with the
4//! [`tracing`] crate and emits debug level tracing data.
5//!
6//! Most users will only use the top-level exported generic [`WebhookServer`]
7//! which enables complete control over the [Router] which handles registering
8//! routes and their handler functions.
9//!
10//! ```
11//! use stackable_webhook::{WebhookServer, WebhookOptions};
12//! use axum::Router;
13//!
14//! # async fn test() {
15//! let router = Router::new();
16//! let (server, cert_rx) = WebhookServer::new(router, WebhookOptions::default())
17//!     .await
18//!     .expect("failed to create WebhookServer");
19//! # }
20//! ```
21//!
22//! For some usages, complete end-to-end [`WebhookServer`] implementations
23//! exist. One such implementation is the [`ConversionWebhookServer`][1].
24//!
25//! This library additionally also exposes lower-level structs and functions to
26//! enable complete control over these details if needed.
27//!
28//! [1]: crate::servers::ConversionWebhookServer
29use axum::{Router, routing::get};
30use futures_util::{FutureExt as _, pin_mut, select};
31use snafu::{ResultExt, Snafu};
32use stackable_telemetry::AxumTraceLayer;
33use tokio::{
34    signal::unix::{SignalKind, signal},
35    sync::mpsc,
36};
37use tower::ServiceBuilder;
38use x509_cert::Certificate;
39
40// use tower_http::trace::TraceLayer;
41use crate::tls::TlsServer;
42
43pub mod constants;
44pub mod options;
45pub mod servers;
46pub mod tls;
47
48// Selected re-exports
49pub use crate::options::WebhookOptions;
50
51/// A generic webhook handler receiving a request and sending back a response.
52///
53/// This trait is not intended to be implemented by external crates and this
54/// library provides various ready-to-use implementations for it. One such an
55/// implementation is part of the [`ConversionWebhookServer`][1].
56///
57/// [1]: crate::servers::ConversionWebhookServer
58pub trait WebhookHandler<Req, Res> {
59    fn call(self, req: Req) -> Res;
60}
61
62/// A result type alias with the [`WebhookError`] type as the default error type.
63pub type Result<T, E = WebhookError> = std::result::Result<T, E>;
64
65#[derive(Debug, Snafu)]
66pub enum WebhookError {
67    #[snafu(display("failed to create TLS server"))]
68    CreateTlsServer { source: tls::TlsServerError },
69
70    #[snafu(display("failed to run TLS server"))]
71    RunTlsServer { source: tls::TlsServerError },
72}
73
74/// A ready-to-use webhook server.
75///
76/// This server abstracts away lower-level details like TLS termination
77/// and other various configurations, validations or middlewares. The routes
78/// and their handlers are completely customizable by bringing your own
79/// Axum [`Router`].
80///
81/// For complete end-to-end implementations, see [`ConversionWebhookServer`][1].
82///
83/// [1]: crate::servers::ConversionWebhookServer
84pub struct WebhookServer {
85    tls_server: TlsServer,
86}
87
88impl WebhookServer {
89    /// Creates a new ready-to-use webhook server.
90    ///
91    /// The server listens on `socket_addr` which is provided via the [`WebhookOptions`] and handles
92    /// routing based on the provided Axum `router`. Most of the time it is sufficient to use
93    /// [`WebhookOptions::default()`]. See the documentation for [`WebhookOptions`] for more details
94    /// on the default values.
95    ///
96    /// To start the server, use the [`WebhookServer::run()`] function. This will
97    /// run the server using the Tokio runtime until it is terminated.
98    ///
99    /// ### Basic Example
100    ///
101    /// ```
102    /// use stackable_webhook::{WebhookServer, WebhookOptions};
103    /// use axum::Router;
104    ///
105    /// # async fn test() {
106    /// let router = Router::new();
107    /// let (server, cert_rx) = WebhookServer::new(router, WebhookOptions::default())
108    ///     .await
109    ///     .expect("failed to create WebhookServer");
110    /// # }
111    /// ```
112    ///
113    /// ### Example with Custom Options
114    ///
115    /// ```
116    /// use stackable_webhook::{WebhookServer, WebhookOptions};
117    /// use axum::Router;
118    ///
119    /// # async fn test() {
120    /// let options = WebhookOptions::builder()
121    ///     .bind_address([127, 0, 0, 1], 8080)
122    ///     .add_subject_alterative_dns_name("my-san-entry")
123    ///     .build();
124    ///
125    /// let router = Router::new();
126    /// let (server, cert_rx) = WebhookServer::new(router, options)
127    ///     .await
128    ///     .expect("failed to create WebhookServer");
129    /// # }
130    /// ```
131    pub async fn new(
132        router: Router,
133        options: WebhookOptions,
134    ) -> Result<(Self, mpsc::Receiver<Certificate>)> {
135        tracing::trace!("create new webhook server");
136
137        // TODO (@Techassi): Make opt-in configurable from the outside
138        // Create an OpenTelemetry tracing layer
139        tracing::trace!("create tracing service (layer)");
140        let trace_layer = AxumTraceLayer::new().with_opt_in();
141
142        // Use a service builder to provide multiple layers at once. Recommended
143        // by the Axum project.
144        //
145        // See https://docs.rs/axum/latest/axum/middleware/index.html#applying-multiple-middleware
146        // TODO (@NickLarsenNZ): rename this server_builder and keep it specific to tracing, since it's placement in the chain is important
147        let service_builder = ServiceBuilder::new().layer(trace_layer);
148
149        // Create the root router and merge the provided router into it.
150        tracing::debug!("create core router and merge provided router");
151        let router = router
152            .layer(service_builder)
153            // The health route is below the AxumTraceLayer so as not to be instrumented
154            .route("/health", get(|| async { "ok" }));
155
156        tracing::debug!("create TLS server");
157        let (tls_server, cert_rx) = TlsServer::new(router, options)
158            .await
159            .context(CreateTlsServerSnafu)?;
160
161        Ok((Self { tls_server }, cert_rx))
162    }
163
164    /// Runs the Webhook server and sets up signal handlers for shutting down.
165    ///
166    /// This does not implement graceful shutdown of the underlying server.
167    pub async fn run(self) -> Result<()> {
168        let future_server = self.run_server();
169        let future_signal = async {
170            let mut sigint = signal(SignalKind::interrupt()).expect("create SIGINT listener");
171            let mut sigterm = signal(SignalKind::terminate()).expect("create SIGTERM listener");
172
173            tracing::debug!("created unix signal handlers");
174
175            select! {
176                signal = sigint.recv().fuse() => {
177                    if signal.is_some() {
178                        tracing::debug!( "received SIGINT");
179                    }
180                },
181                signal = sigterm.recv().fuse() => {
182                    if signal.is_some() {
183                        tracing::debug!( "received SIGTERM");
184                    }
185                },
186            };
187        };
188
189        // select requires Future + Unpin
190        pin_mut!(future_server);
191        pin_mut!(future_signal);
192
193        futures_util::future::select(future_server, future_signal).await;
194
195        Ok(())
196    }
197
198    /// Runs the webhook server by creating a TCP listener and binding it to
199    /// the specified socket address.
200    async fn run_server(self) -> Result<()> {
201        tracing::debug!("run webhook server");
202
203        self.tls_server.run().await.context(RunTlsServerSnafu)
204    }
205}