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}