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
291use std:: net:: { IpAddr , Ipv4Addr , SocketAddr } ;
302
313use :: x509_cert:: Certificate ;
324use axum:: { Router , routing:: get} ;
33- use futures_util:: { FutureExt as _, pin_mut, select} ;
5+ use futures_util:: { FutureExt as _, TryFutureExt , select} ;
6+ use k8s_openapi:: ByteString ;
7+ use servers:: { WebhookServerImplementation , WebhookServerImplementationError } ;
348use snafu:: { ResultExt , Snafu } ;
359use stackable_telemetry:: AxumTraceLayer ;
3610use tokio:: {
3711 signal:: unix:: { SignalKind , signal} ,
3812 sync:: mpsc,
13+ try_join,
3914} ;
4015use tower:: ServiceBuilder ;
16+ use x509_cert:: der:: { EncodePem , pem:: LineEnding } ;
4117
42- // Selected re-exports
43- pub use crate :: options:: WebhookOptions ;
4418use crate :: tls:: TlsServer ;
4519
46- pub mod maintainer;
47- pub mod options;
4820pub mod servers;
4921pub mod tls;
5022
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
58- pub trait WebhookHandler < Req , Res > {
59- fn call ( self , req : Req ) -> Res ;
60- }
61-
6223/// A result type alias with the [`WebhookError`] type as the default error type.
6324pub type Result < T , E = WebhookError > = std:: result:: Result < T , E > ;
6425
@@ -69,24 +30,38 @@ pub enum WebhookError {
6930
7031 #[ snafu( display( "failed to run TLS server" ) ) ]
7132 RunTlsServer { source : tls:: TlsServerError } ,
33+
34+ #[ snafu( display( "failed to update certificate" ) ) ]
35+ UpdateCertificate {
36+ source : WebhookServerImplementationError ,
37+ } ,
38+
39+ #[ snafu( display( "failed to encode CA certificate as PEM format" ) ) ]
40+ EncodeCertificateAuthorityAsPem { source : x509_cert:: der:: Error } ,
7241}
7342
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
8443pub struct WebhookServer {
44+ options : WebhookOptions ,
45+ webhooks : Vec < Box < dyn WebhookServerImplementation > > ,
8546 tls_server : TlsServer ,
47+ cert_rx : mpsc:: Receiver < Certificate > ,
48+ }
49+
50+ #[ derive( Clone , Debug ) ]
51+ pub struct WebhookOptions {
52+ /// The default HTTPS socket address the [`TcpListener`][tokio::net::TcpListener]
53+ /// binds to.
54+ pub socket_addr : SocketAddr ,
55+
56+ /// The namespace the operator/webhook is running in.
57+ pub operator_namespace : String ,
58+
59+ /// The name of the Kubernetes service which points to the operator/webhook.
60+ pub operator_service_name : String ,
8661}
8762
8863impl WebhookServer {
89- /// The default HTTPS port `8443`
64+ /// The default HTTPS port
9065 pub const DEFAULT_HTTPS_PORT : u16 = 8443 ;
9166 /// The default IP address [`Ipv4Addr::UNSPECIFIED`] (`0.0.0.0`) the webhook server binds to,
9267 /// which represents binding on all network addresses.
@@ -99,52 +74,10 @@ impl WebhookServer {
9974 pub const DEFAULT_SOCKET_ADDRESS : SocketAddr =
10075 SocketAddr :: new ( Self :: DEFAULT_LISTEN_ADDRESS , Self :: DEFAULT_HTTPS_PORT ) ;
10176
102- /// Creates a new ready-to-use webhook server.
103- ///
104- /// The server listens on `socket_addr` which is provided via the [`WebhookOptions`] and handles
105- /// routing based on the provided Axum `router`. Most of the time it is sufficient to use
106- /// [`WebhookOptions::default()`]. See the documentation for [`WebhookOptions`] for more details
107- /// on the default values.
108- ///
109- /// To start the server, use the [`WebhookServer::run()`] function. This will
110- /// run the server using the Tokio runtime until it is terminated.
111- ///
112- /// ### Basic Example
113- ///
114- /// ```
115- /// use stackable_webhook::{WebhookServer, WebhookOptions};
116- /// use axum::Router;
117- ///
118- /// # async fn test() {
119- /// let router = Router::new();
120- /// let (server, cert_rx) = WebhookServer::new(router, WebhookOptions::default())
121- /// .await
122- /// .expect("failed to create WebhookServer");
123- /// # }
124- /// ```
125- ///
126- /// ### Example with Custom Options
127- ///
128- /// ```
129- /// use stackable_webhook::{WebhookServer, WebhookOptions};
130- /// use axum::Router;
131- ///
132- /// # async fn test() {
133- /// let options = WebhookOptions::builder()
134- /// .bind_address([127, 0, 0, 1], 8080)
135- /// .add_subject_alterative_dns_name("my-san-entry")
136- /// .build();
137- ///
138- /// let router = Router::new();
139- /// let (server, cert_rx) = WebhookServer::new(router, options)
140- /// .await
141- /// .expect("failed to create WebhookServer");
142- /// # }
143- /// ```
14477 pub async fn new (
145- router : Router ,
14678 options : WebhookOptions ,
147- ) -> Result < ( Self , mpsc:: Receiver < Certificate > ) > {
79+ webhooks : Vec < Box < dyn WebhookServerImplementation > > ,
80+ ) -> Result < Self > {
14881 tracing:: trace!( "create new webhook server" ) ;
14982
15083 // TODO (@Techassi): Make opt-in configurable from the outside
@@ -161,17 +94,26 @@ impl WebhookServer {
16194
16295 // Create the root router and merge the provided router into it.
16396 tracing:: debug!( "create core router and merge provided router" ) ;
164- let router = router
97+ let mut router = Router :: new ( )
16598 . layer ( service_builder)
16699 // The health route is below the AxumTraceLayer so as not to be instrumented
167100 . route ( "/health" , get ( || async { "ok" } ) ) ;
168101
102+ for webhook in webhooks. iter ( ) {
103+ router = webhook. register_routes ( router) ;
104+ }
105+
169106 tracing:: debug!( "create TLS server" ) ;
170- let ( tls_server, cert_rx) = TlsServer :: new ( router, options)
107+ let ( tls_server, cert_rx) = TlsServer :: new ( router, options. clone ( ) )
171108 . await
172109 . context ( CreateTlsServerSnafu ) ?;
173110
174- Ok ( ( Self { tls_server } , cert_rx) )
111+ Ok ( Self {
112+ options,
113+ webhooks,
114+ tls_server,
115+ cert_rx,
116+ } )
175117 }
176118
177119 /// Runs the Webhook server and sets up signal handlers for shutting down.
@@ -200,19 +142,59 @@ impl WebhookServer {
200142 } ;
201143
202144 // select requires Future + Unpin
203- pin_mut ! ( future_server) ;
204- pin_mut ! ( future_signal) ;
205-
206- futures_util:: future:: select ( future_server, future_signal) . await ;
145+ tokio:: pin!( future_server) ;
146+ tokio:: pin!( future_signal) ;
147+
148+ tokio:: select! {
149+ res = & mut future_server => {
150+ // If the server future errors, propagate the error
151+ res?;
152+ }
153+ _ = & mut future_signal => {
154+ tracing:: info!( "shutdown signal received, stopping server" ) ;
155+ }
156+ }
207157
208158 Ok ( ( ) )
209159 }
210160
211- /// Runs the webhook server by creating a TCP listener and binding it to
212- /// the specified socket address.
213161 async fn run_server ( self ) -> Result < ( ) > {
214162 tracing:: debug!( "run webhook server" ) ;
215163
216- self . tls_server . run ( ) . await . context ( RunTlsServerSnafu )
164+ let Self {
165+ options,
166+ mut webhooks,
167+ tls_server,
168+ mut cert_rx,
169+ // initial_reconcile_tx,
170+ } = self ;
171+ let tls_server = tls_server
172+ . run ( )
173+ . map_err ( |err| WebhookError :: RunTlsServer { source : err } ) ;
174+
175+ let cert_update_loop = async {
176+ loop {
177+ while let Some ( cert) = cert_rx. recv ( ) . await {
178+ // The caBundle needs to be provided as a base64-encoded PEM envelope.
179+ let ca_bundle = cert
180+ . to_pem ( LineEnding :: LF )
181+ . context ( EncodeCertificateAuthorityAsPemSnafu ) ?;
182+ let ca_bundle = ByteString ( ca_bundle. as_bytes ( ) . to_vec ( ) ) ;
183+
184+ for webhook in webhooks. iter_mut ( ) {
185+ webhook
186+ . handle_certificate_rotation ( & cert, & ca_bundle, & options)
187+ . await
188+ . context ( UpdateCertificateSnafu ) ?;
189+ }
190+ }
191+ }
192+
193+ // We need to hint the return type to the compiler
194+ #[ allow( unreachable_code) ]
195+ Ok ( ( ) )
196+ } ;
197+
198+ try_join ! ( cert_update_loop, tls_server) . map ( |_| ( ) )
217199 }
218200}
0 commit comments