From a1de224d5030cc04f7867dba607e92d571ea6d7a Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Fri, 26 Jan 2024 11:19:25 +0100
Subject: [PATCH 01/31] Initial commit

---
 Cargo.toml                          |  2 +-
 stackable-webhook/Cargo.toml        | 13 +++++++++
 stackable-webhook/src/conversion.rs | 42 ++++++++++++++++++++++++++++
 stackable-webhook/src/lib.rs        | 43 +++++++++++++++++++++++++++++
 4 files changed, 99 insertions(+), 1 deletion(-)
 create mode 100644 stackable-webhook/Cargo.toml
 create mode 100644 stackable-webhook/src/conversion.rs
 create mode 100644 stackable-webhook/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index db3161da1..903142512 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -67,4 +67,4 @@ rstest = "0.18.1"
 tempfile = "3.7.1"
 
 [workspace]
-members = ["stackable-operator-derive"]
+members = ["stackable-operator-derive", "stackable-webhook"]
diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml
new file mode 100644
index 000000000..2cbdd04fe
--- /dev/null
+++ b/stackable-webhook/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "stackable-webhook"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+repository.workspace = true
+
+[dependencies]
+axum = "0.7.4"
+kube = { version = "0.87.1", default-features = false }
+serde_json = "1.0.104"
+tokio = "1.29.1"
diff --git a/stackable-webhook/src/conversion.rs b/stackable-webhook/src/conversion.rs
new file mode 100644
index 000000000..3f0ab8421
--- /dev/null
+++ b/stackable-webhook/src/conversion.rs
@@ -0,0 +1,42 @@
+use std::{net::SocketAddr, ops::Deref};
+
+use axum::{
+    routing::{post, MethodRouter},
+    Json,
+};
+use kube::core::conversion::{ConversionRequest, ConversionResponse};
+
+use crate::{Handlers, WebhookServer};
+
+pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>);
+
+impl Deref for ConversionWebhookServer {
+    type Target = WebhookServer<ConversionHandlers>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl ConversionWebhookServer {
+    pub async fn new(socket_addr: SocketAddr) -> Self {
+        Self(WebhookServer::new(socket_addr, ConversionHandlers).await)
+    }
+}
+
+pub struct ConversionHandlers;
+
+impl Handlers for ConversionHandlers {
+    fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)>
+    where
+        T: Clone + Sync + Send + 'static,
+    {
+        vec![("/convert", post(convert_handler))]
+    }
+}
+
+async fn convert_handler(
+    Json(_conversion_request): Json<ConversionRequest>,
+) -> Json<ConversionResponse> {
+    todo!()
+}
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
new file mode 100644
index 000000000..164d81bbc
--- /dev/null
+++ b/stackable-webhook/src/lib.rs
@@ -0,0 +1,43 @@
+use std::net::SocketAddr;
+
+use axum::{routing::MethodRouter, Router};
+use tokio::net::TcpListener;
+
+pub mod conversion;
+
+pub trait Handlers {
+    fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)>
+    where
+        T: Clone + Sync + Send + 'static;
+}
+
+pub struct WebhookServer<T>
+where
+    T: Handlers,
+{
+    socket_addr: SocketAddr,
+    handlers: T,
+}
+
+impl<T> WebhookServer<T>
+where
+    T: Handlers,
+{
+    pub async fn new(socket_addr: SocketAddr, handlers: T) -> Self {
+        Self {
+            socket_addr,
+            handlers,
+        }
+    }
+
+    pub async fn run(&self) {
+        let mut router = Router::new();
+
+        for (path, method_router) in self.handlers.endpoints() {
+            router = router.route(path, method_router)
+        }
+
+        let listener = TcpListener::bind(self.socket_addr).await.unwrap();
+        axum::serve(listener, router).await.unwrap()
+    }
+}

From 13612c16938a0738ee576022814af60953a58359 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Mon, 5 Feb 2024 13:16:59 +0100
Subject: [PATCH 02/31] Start redirector impl

---
 stackable-webhook/Cargo.toml      |  3 ++
 stackable-webhook/src/lib.rs      | 12 +----
 stackable-webhook/src/redirect.rs | 90 +++++++++++++++++++++++++++++++
 3 files changed, 94 insertions(+), 11 deletions(-)
 create mode 100644 stackable-webhook/src/redirect.rs

diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml
index 2cbdd04fe..d4e0e3847 100644
--- a/stackable-webhook/Cargo.toml
+++ b/stackable-webhook/Cargo.toml
@@ -10,4 +10,7 @@ repository.workspace = true
 axum = "0.7.4"
 kube = { version = "0.87.1", default-features = false }
 serde_json = "1.0.104"
+snafu = "0.8.0"
 tokio = "1.29.1"
+tower = "0.4.13"
+tracing = "0.1.40"
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 164d81bbc..59d9d101d 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -4,17 +4,7 @@ use axum::{routing::MethodRouter, Router};
 use tokio::net::TcpListener;
 
 pub mod conversion;
-
-pub trait Handlers {
-    fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)>
-    where
-        T: Clone + Sync + Send + 'static;
-}
-
-pub struct WebhookServer<T>
-where
-    T: Handlers,
-{
+pub mod redirect;
     socket_addr: SocketAddr,
     handlers: T,
 }
diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs
new file mode 100644
index 000000000..01ea3e4c9
--- /dev/null
+++ b/stackable-webhook/src/redirect.rs
@@ -0,0 +1,90 @@
+use std::net::{IpAddr, SocketAddr};
+
+use axum::{
+    extract::Host,
+    handler::HandlerWithoutStateExt,
+    http::{
+        uri::{InvalidUri, InvalidUriParts, Scheme},
+        StatusCode, Uri,
+    },
+    response::Redirect,
+};
+use snafu::{ResultExt, Snafu};
+use tokio::net::TcpListener;
+use tracing::warn;
+
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("failed to parse HTTPS host as authority"))]
+    ParseAuthority { source: InvalidUri },
+
+    #[snafu(display("failed to convert URI parts into URI"))]
+    ConvertPartsToUri { source: InvalidUriParts },
+}
+
+/// A redirector which redirects HTTP connections at "/" to HTTPS automatically.
+///
+/// Internally it uses a simple handler function which is registered as a
+/// singular [`Service`][tower::MakeService] at the root "/" path. If the
+/// conversion from HTTP to HTTPS fails, the [`Redirector`] returns a HTTP
+/// status code 400 (Bad Request). Additionally, a warning trace is emitted.
+pub struct Redirector {
+    ip_addr: IpAddr,
+    https_port: u16,
+    http_port: u16,
+}
+
+impl Redirector {
+    pub fn new(ip_addr: IpAddr, http_port: u16, https_port: u16) -> Self {
+        Self {
+            https_port,
+            http_port,
+            ip_addr,
+        }
+    }
+
+    pub async fn run(self) {
+        // The redirector only binds to the HTTP port. The actual HTTPS
+        // application runs in a separate task and is completely independent
+        // of this redirector.
+        let socket_addr = SocketAddr::new(self.ip_addr, self.http_port);
+        let listener = TcpListener::bind(socket_addr).await.unwrap();
+
+        // This converts the HTTP request URI into HTTPS. If this fails, the
+        // redirector emits a warning trace and returns HTTP status code 400
+        // (Bad Request).
+        let redirect = move |Host(host): Host, uri: Uri| async move {
+            match http_to_https(host, uri, self.http_port, self.https_port) {
+                Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
+                Err(err) => {
+                    warn!(%err, "failed to convert HTTP URI to HTTPS");
+                    Err(StatusCode::BAD_REQUEST)
+                }
+            }
+        };
+
+        // This registers the handler function as the only handler at the root
+        // path "/". See https://docs.rs/axum/latest/axum/fn.serve.html#examples
+        axum::serve(listener, redirect.into_make_service())
+            .await
+            .unwrap();
+    }
+}
+
+fn http_to_https(host: String, uri: Uri, http_port: u16, https_port: u16) -> Result<Uri, Error> {
+    let mut parts = uri.into_parts();
+
+    parts.scheme = Some(Scheme::HTTPS);
+
+    if parts.path_and_query.is_none() {
+        // NOTE (@Techassi): This should never fail and is this save to unwrap.
+        // If this will change into a user-controlled value, then this isn't
+        // save to unwrap anymore and will require explicit error handling.
+        parts.path_and_query = Some("/".parse().unwrap());
+    }
+
+    let https_host = host.replace(&http_port.to_string(), &https_port.to_string());
+    parts.authority = Some(https_host.parse().context(ParseAuthoritySnafu)?);
+
+    Ok(Uri::from_parts(parts).context(ConvertPartsToUriSnafu)?)
+}

From 66a54ae2ad18f6bd0b5b8a1a47849d963964d417 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Mon, 5 Feb 2024 15:28:41 +0100
Subject: [PATCH 03/31] Improve redirector impl

---
 stackable-webhook/src/constants.rs |   4 ++
 stackable-webhook/src/lib.rs       | 108 ++++++++++++++++++++++++-----
 stackable-webhook/src/redirect.rs  |   2 +-
 3 files changed, 95 insertions(+), 19 deletions(-)
 create mode 100644 stackable-webhook/src/constants.rs

diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs
new file mode 100644
index 000000000..b1c53a526
--- /dev/null
+++ b/stackable-webhook/src/constants.rs
@@ -0,0 +1,4 @@
+pub const DEFAULT_HTTPS_PORT: u16 = 443;
+pub const DEFAULT_HTTP_PORT: u16 = 80;
+
+pub const DEFAULT_IP_ADDRESS: [u8; 4] = [127, 0, 0, 1];
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 59d9d101d..d9fc64187 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -1,33 +1,105 @@
-use std::net::SocketAddr;
+use std::{net::SocketAddr, sync::Arc};
 
-use axum::{routing::MethodRouter, Router};
+use axum::Router;
 use tokio::net::TcpListener;
+use tokio_rustls::rustls::ServerConfig;
+use tracing::warn;
 
+use crate::{
+    constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS},
+    redirect::Redirector,
+};
+
+pub mod constants;
 pub mod conversion;
 pub mod redirect;
-    socket_addr: SocketAddr,
-    handlers: T,
+
+/// A ready-to-use webhook server.
+pub struct WebhookServer {
+    options: Options,
+    router: Router,
 }
 
-impl<T> WebhookServer<T>
-where
-    T: Handlers,
-{
-    pub async fn new(socket_addr: SocketAddr, handlers: T) -> Self {
-        Self {
-            socket_addr,
-            handlers,
-        }
+impl WebhookServer {
+    /// Creates a new ready-to-use webhook server.
+    ///
+    /// The server listens on `socket_addr` which is provided via the [`Options`]
+    /// and handles routing based on the provided Axum `router`. Most of the time
+    /// it is sufficient to use [`Options::default()`]. See the documentation
+    /// for [`Options`] for more details on the default values.
+    pub async fn new(router: Router, options: Options) -> Self {
+        Self { options, router }
     }
 
-    pub async fn run(&self) {
-        let mut router = Router::new();
+    /// Runs the webhook server by creating a TCP listener and binding it to
+    /// the specified socket address.
+    pub async fn run(self) {
+        // Only run the auto redirector when enabled
+        match self.options.redirect {
+            RedirectOption::Enabled(http_port) => {
+                let redirector = Redirector::new(
+                    self.options.socket_addr.ip(),
+                    self.options.socket_addr.port(),
+                    http_port,
+                );
 
-        for (path, method_router) in self.handlers.endpoints() {
-            router = router.route(path, method_router)
+                tokio::spawn(redirector.run());
+            }
+            RedirectOption::Disabled => {
+                warn!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended");
+            }
         }
 
-        let listener = TcpListener::bind(self.socket_addr).await.unwrap();
+        let mut router = Router::new();
+        router = router.merge(self.router);
+
+        let listener = TcpListener::bind(self.options.socket_addr).await.unwrap();
         axum::serve(listener, router).await.unwrap()
     }
 }
+
+pub struct TlsServer {
+    config: Arc<ServerConfig>,
+}
+
+impl TlsServer {
+    // pub fn new() -> Self {
+    //     let config = ServerConfig::builder()
+    //         .with_no_client_auth()
+    //         .with_cert_resolver(cert_resolver);
+    //     let config = Arc::new(config);
+
+    //     Self { config }
+    // }
+}
+
+/// Specifies available webhook server options.
+///
+/// The [`Default`] implemention for this struct contains the following
+/// values:
+///
+/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080
+/// - The socket binds to 127.0.0.1 on port 8443 (HTTPS)
+pub struct Options {
+    /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled,
+    /// it is required to specify the HTTP port.
+    pub redirect: RedirectOption,
+
+    /// The default HTTPS socket address the [`TcpListener`] binds to. The same
+    /// IP adress is used for the auto HTTP to HTTPS redirect handler.
+    pub socket_addr: SocketAddr,
+}
+
+impl Default for Options {
+    fn default() -> Self {
+        Self {
+            socket_addr: SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT)),
+            redirect: RedirectOption::Enabled(DEFAULT_HTTP_PORT),
+        }
+    }
+}
+
+pub enum RedirectOption {
+    Enabled(u16),
+    Disabled,
+}
diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs
index 01ea3e4c9..c3b1a8607 100644
--- a/stackable-webhook/src/redirect.rs
+++ b/stackable-webhook/src/redirect.rs
@@ -35,7 +35,7 @@ pub struct Redirector {
 }
 
 impl Redirector {
-    pub fn new(ip_addr: IpAddr, http_port: u16, https_port: u16) -> Self {
+    pub fn new(ip_addr: IpAddr, https_port: u16, http_port: u16) -> Self {
         Self {
             https_port,
             http_port,

From 5dc336e4e200875b36797c4e018f0edb61f8a67d Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 09:35:57 +0100
Subject: [PATCH 04/31] Add doc comments, add traces

---
 stackable-webhook/src/lib.rs      | 25 ++++++++++++++++++++++++-
 stackable-webhook/src/redirect.rs | 18 +++++++++++++++---
 2 files changed, 39 insertions(+), 4 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index d9fc64187..28e13b197 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -1,9 +1,17 @@
+//! Utility types and functions to easily create ready-to-use webhook servers
+//! which can handle different tasks, for example CRD conversions. All webhook
+//! servers use HTTPS per default and provide options to enable HTTP to HTTPS
+//! redirection as well.
+//!
+//! The crate is also fully compatible with [`tracing`], and emits multiple
+//! levels of tracing data.
+
 use std::{net::SocketAddr, sync::Arc};
 
 use axum::Router;
 use tokio::net::TcpListener;
 use tokio_rustls::rustls::ServerConfig;
-use tracing::warn;
+use tracing::{debug, warn};
 
 use crate::{
     constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS},
@@ -27,16 +35,31 @@ impl WebhookServer {
     /// and handles routing based on the provided Axum `router`. Most of the time
     /// it is sufficient to use [`Options::default()`]. See the documentation
     /// for [`Options`] for more details on the default values.
+    ///
+    /// ### Example
+    ///
+    /// ```
+    /// use stackable_webhook::{WebhookServer, Options};
+    /// use axum::Router;
+    ///
+    /// let router = Router::new();
+    /// let server = WebhookServer::new(router, Options::default());
+    /// ```
     pub async fn new(router: Router, options: Options) -> Self {
+        debug!("create new webhook server");
         Self { options, router }
     }
 
     /// Runs the webhook server by creating a TCP listener and binding it to
     /// the specified socket address.
     pub async fn run(self) {
+        debug!("run webhook server");
+
         // Only run the auto redirector when enabled
         match self.options.redirect {
             RedirectOption::Enabled(http_port) => {
+                debug!("run webhook server with automatic HTTP to HTTPS redirect enabled");
+
                 let redirector = Redirector::new(
                     self.options.socket_addr.ip(),
                     self.options.socket_addr.port(),
diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs
index c3b1a8607..1774bc2b0 100644
--- a/stackable-webhook/src/redirect.rs
+++ b/stackable-webhook/src/redirect.rs
@@ -11,7 +11,7 @@ use axum::{
 };
 use snafu::{ResultExt, Snafu};
 use tokio::net::TcpListener;
-use tracing::warn;
+use tracing::{debug, info, instrument, warn};
 
 #[derive(Debug, Snafu)]
 pub enum Error {
@@ -28,6 +28,7 @@ pub enum Error {
 /// singular [`Service`][tower::MakeService] at the root "/" path. If the
 /// conversion from HTTP to HTTPS fails, the [`Redirector`] returns a HTTP
 /// status code 400 (Bad Request). Additionally, a warning trace is emitted.
+#[derive(Debug)]
 pub struct Redirector {
     ip_addr: IpAddr,
     https_port: u16,
@@ -35,7 +36,10 @@ pub struct Redirector {
 }
 
 impl Redirector {
+    #[instrument]
     pub fn new(ip_addr: IpAddr, https_port: u16, http_port: u16) -> Self {
+        debug!("create new HTTP to HTTPS redirector");
+
         Self {
             https_port,
             http_port,
@@ -43,7 +47,10 @@ impl Redirector {
         }
     }
 
+    #[instrument]
     pub async fn run(self) {
+        debug!("run redirector");
+
         // The redirector only binds to the HTTP port. The actual HTTPS
         // application runs in a separate task and is completely independent
         // of this redirector.
@@ -54,8 +61,13 @@ impl Redirector {
         // redirector emits a warning trace and returns HTTP status code 400
         // (Bad Request).
         let redirect = move |Host(host): Host, uri: Uri| async move {
-            match http_to_https(host, uri, self.http_port, self.https_port) {
-                Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
+            // NOTE (@Techassi): Is it worth to clone here just to be able to
+            // print it in the trace?
+            match http_to_https(host, uri.clone(), self.http_port, self.https_port) {
+                Ok(redirect_uri) => {
+                    info!("redirecting from {} to {}", uri, redirect_uri);
+                    Ok(Redirect::permanent(&redirect_uri.to_string()))
+                }
                 Err(err) => {
                     warn!(%err, "failed to convert HTTP URI to HTTPS");
                     Err(StatusCode::BAD_REQUEST)

From 74b874ad669b5870f08fec3344f975a7ed77701b Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 11:14:09 +0100
Subject: [PATCH 05/31] Add options builder, move into own module

---
 stackable-webhook/src/lib.rs     |  67 ++++++++------------
 stackable-webhook/src/options.rs | 104 +++++++++++++++++++++++++++++++
 2 files changed, 131 insertions(+), 40 deletions(-)
 create mode 100644 stackable-webhook/src/options.rs

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 28e13b197..df8bdac05 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -6,21 +6,20 @@
 //! The crate is also fully compatible with [`tracing`], and emits multiple
 //! levels of tracing data.
 
-use std::{net::SocketAddr, sync::Arc};
+use std::sync::Arc;
 
 use axum::Router;
 use tokio::net::TcpListener;
 use tokio_rustls::rustls::ServerConfig;
 use tracing::{debug, warn};
 
-use crate::{
-    constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS},
-    redirect::Redirector,
-};
-
 pub mod constants;
 pub mod conversion;
-pub mod redirect;
+mod options;
+mod redirect;
+
+pub use options::*;
+pub use redirect::*;
 
 /// A ready-to-use webhook server.
 pub struct WebhookServer {
@@ -36,7 +35,10 @@ impl WebhookServer {
     /// it is sufficient to use [`Options::default()`]. See the documentation
     /// for [`Options`] for more details on the default values.
     ///
-    /// ### Example
+    /// To start the server, use the [`WebhookServer::run()`] function. This will
+    /// run the server using the Tokio runtime until it is terminated.
+    ///
+    /// ### Basic Example
     ///
     /// ```
     /// use stackable_webhook::{WebhookServer, Options};
@@ -45,7 +47,22 @@ impl WebhookServer {
     /// let router = Router::new();
     /// let server = WebhookServer::new(router, Options::default());
     /// ```
-    pub async fn new(router: Router, options: Options) -> Self {
+    ///
+    /// ### Example with Custom Options
+    ///
+    /// ```
+    /// use stackable_webhook::{WebhookServer, Options};
+    /// use axum::Router;
+    ///
+    /// let options = Options::builder()
+    ///     .disable_redirect()
+    ///     .socket_addr(([127, 0, 0, 1], 8080))
+    ///     .build();
+    ///
+    /// let router = Router::new();
+    /// let server = WebhookServer::new(router, options);
+    /// ```
+    pub fn new(router: Router, options: Options) -> Self {
         debug!("create new webhook server");
         Self { options, router }
     }
@@ -73,6 +90,7 @@ impl WebhookServer {
             }
         }
 
+        // Create the root router and merge the provided router into it.
         let mut router = Router::new();
         router = router.merge(self.router);
 
@@ -95,34 +113,3 @@ impl TlsServer {
     //     Self { config }
     // }
 }
-
-/// Specifies available webhook server options.
-///
-/// The [`Default`] implemention for this struct contains the following
-/// values:
-///
-/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080
-/// - The socket binds to 127.0.0.1 on port 8443 (HTTPS)
-pub struct Options {
-    /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled,
-    /// it is required to specify the HTTP port.
-    pub redirect: RedirectOption,
-
-    /// The default HTTPS socket address the [`TcpListener`] binds to. The same
-    /// IP adress is used for the auto HTTP to HTTPS redirect handler.
-    pub socket_addr: SocketAddr,
-}
-
-impl Default for Options {
-    fn default() -> Self {
-        Self {
-            socket_addr: SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT)),
-            redirect: RedirectOption::Enabled(DEFAULT_HTTP_PORT),
-        }
-    }
-}
-
-pub enum RedirectOption {
-    Enabled(u16),
-    Disabled,
-}
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
new file mode 100644
index 000000000..1a665d748
--- /dev/null
+++ b/stackable-webhook/src/options.rs
@@ -0,0 +1,104 @@
+use std::net::SocketAddr;
+
+use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS};
+
+/// Specifies available webhook server options.
+///
+/// The [`Default`] implemention for this struct contains the following
+/// values:
+///
+/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080
+/// - The socket binds to 127.0.0.1 on port 8443 (HTTPS)
+pub struct Options {
+    /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled,
+    /// it is required to specify the HTTP port.
+    pub redirect: RedirectOption,
+
+    /// The default HTTPS socket address the [`TcpListener`] binds to. The same
+    /// IP adress is used for the auto HTTP to HTTPS redirect handler.
+    pub socket_addr: SocketAddr,
+
+    /// Either auto-generate or use an injected TLS certificate.
+    pub tls: TlsOption,
+}
+
+impl Default for Options {
+    fn default() -> Self {
+        Self::builder().build()
+    }
+}
+
+impl Options {
+    pub fn builder() -> OptionsBuilder {
+        OptionsBuilder::default()
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct OptionsBuilder {
+    redirect: Option<RedirectOption>,
+    socket_addr: Option<SocketAddr>,
+    tls: Option<TlsOption>,
+}
+
+impl OptionsBuilder {
+    pub fn redirect(mut self, redirect: RedirectOption) -> Self {
+        self.redirect = Some(redirect);
+        self
+    }
+
+    pub fn disable_redirect(self) -> Self {
+        self.redirect(RedirectOption::Disabled)
+    }
+
+    pub fn enable_redirect(self, http_port: u16) -> Self {
+        self.redirect(RedirectOption::Enabled(http_port))
+    }
+
+    pub fn socket_addr<T>(mut self, socket_addr: T) -> Self
+    where
+        T: Into<SocketAddr>,
+    {
+        self.socket_addr = Some(socket_addr.into());
+        self
+    }
+
+    pub fn tls(mut self, tls: TlsOption) -> Self {
+        self.tls = Some(tls);
+        self
+    }
+
+    pub fn build(self) -> Options {
+        Options {
+            redirect: self.redirect.unwrap_or_default(),
+            socket_addr: self
+                .socket_addr
+                .unwrap_or(SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT))),
+            tls: self.tls.unwrap_or_default(),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum RedirectOption {
+    Enabled(u16),
+    Disabled,
+}
+
+impl Default for RedirectOption {
+    fn default() -> Self {
+        Self::Enabled(DEFAULT_HTTP_PORT)
+    }
+}
+
+#[derive(Debug)]
+pub enum TlsOption {
+    AutoGenerate,
+    Inject,
+}
+
+impl Default for TlsOption {
+    fn default() -> Self {
+        Self::AutoGenerate
+    }
+}

From bda6e597a7189890241e69d25734e9b9a3d36a10 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 12:17:11 +0100
Subject: [PATCH 06/31] Add initial TLS acceptor

---
 stackable-webhook/Cargo.toml |  11 ++++
 stackable-webhook/src/lib.rs | 116 ++++++++++++++++++++++++++++++++---
 2 files changed, 119 insertions(+), 8 deletions(-)

diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml
index d4e0e3847..807719341 100644
--- a/stackable-webhook/Cargo.toml
+++ b/stackable-webhook/Cargo.toml
@@ -9,8 +9,19 @@ repository.workspace = true
 [dependencies]
 axum = "0.7.4"
 kube = { version = "0.87.1", default-features = false }
+tokio-rustls = "0.24.1"
 serde_json = "1.0.104"
 snafu = "0.8.0"
 tokio = "1.29.1"
+tokio-test = "0.4.3"
 tower = "0.4.13"
 tracing = "0.1.40"
+rustls-pemfile = "1.0.4"
+futures-util = "0.3.30"
+hyper-util = "0.1.3"
+hyper = { version = "1.0.0", features = ["full"] }
+
+[dev-dependencies]
+k8s-openapi = { version = "0.20.0", default-features = false, features = [
+  "v1_28",
+] }
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index df8bdac05..6a451c2a4 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -6,12 +6,20 @@
 //! The crate is also fully compatible with [`tracing`], and emits multiple
 //! levels of tracing data.
 
-use std::sync::Arc;
+use std::{fs::File, io::BufReader, sync::Arc};
 
-use axum::Router;
+use axum::{extract::Request, Router};
+use futures_util::pin_mut;
+use hyper::body::Incoming;
+use hyper_util::rt::{TokioExecutor, TokioIo};
+use rustls_pemfile::{certs, pkcs8_private_keys};
 use tokio::net::TcpListener;
-use tokio_rustls::rustls::ServerConfig;
-use tracing::{debug, warn};
+use tokio_rustls::{
+    rustls::{Certificate, PrivateKey, ServerConfig},
+    TlsAcceptor,
+};
+use tower::Service;
+use tracing::{debug, error, warn};
 
 pub mod constants;
 pub mod conversion;
@@ -90,12 +98,81 @@ impl WebhookServer {
             }
         }
 
+        let mut cert_file =
+            &mut BufReader::new(File::open("/tmp/webhook-certs/serverCert.pem").unwrap());
+        let mut key_file =
+            &mut BufReader::new(File::open("/tmp/webhook-certs/serverKey.pem").unwrap());
+
+        let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0));
+        let certs = certs(&mut cert_file)
+            .unwrap()
+            .into_iter()
+            .map(Certificate)
+            .collect();
+
+        let mut config = ServerConfig::builder()
+            .with_safe_defaults()
+            .with_no_client_auth()
+            .with_single_cert(certs, key)
+            .expect("bad certificate/key");
+
+        config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+
+        let config = Arc::new(config);
+        let tls_acceptor = TlsAcceptor::from(config);
+
         // Create the root router and merge the provided router into it.
-        let mut router = Router::new();
-        router = router.merge(self.router);
+        let mut app = Router::new();
+        app = app.merge(self.router);
+
+        let tcp_listener = TcpListener::bind(self.options.socket_addr).await.unwrap();
+        println!("Binded");
+
+        pin_mut!(tcp_listener);
+        loop {
+            let tower_service = app.clone();
+            let tls_acceptor = tls_acceptor.clone();
+
+            // Wait for new tcp connection
+            let (cnx, addr) = tcp_listener.accept().await.unwrap();
+            println!("TCP Accepted");
+
+            tokio::spawn(async move {
+                // Wait for tls handshake to happen
+                let Ok(stream) = tls_acceptor.accept(cnx).await else {
+                    error!("error during tls handshake connection from {}", addr);
+                    return;
+                };
+
+                println!("TLS Accepted");
+
+                // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio.
+                // `TokioIo` converts between them.
+                let stream = TokioIo::new(stream);
 
-        let listener = TcpListener::bind(self.options.socket_addr).await.unwrap();
-        axum::serve(listener, router).await.unwrap()
+                // Hyper also has its own `Service` trait and doesn't use tower. We can use
+                // `hyper::service::service_fn` to create a hyper `Service` that calls our app through
+                // `tower::Service::call`.
+                let hyper_service =
+                    hyper::service::service_fn(move |request: Request<Incoming>| {
+                        // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
+                        // tower's `Service` requires `&mut self`.
+                        //
+                        // We don't need to call `poll_ready` since `Router` is always ready.
+                        tower_service.clone().call(request)
+                    });
+
+                let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
+                    .serve_connection_with_upgrades(stream, hyper_service)
+                    .await;
+
+                if let Err(err) = ret {
+                    warn!("error serving connection from {}: {}", addr, err);
+                }
+            });
+        }
+
+        // axum::serve(listener, router).await.unwrap()
     }
 }
 
@@ -113,3 +190,26 @@ impl TlsServer {
     //     Self { config }
     // }
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use axum::{routing::post, Router};
+
+    #[tokio::test]
+    async fn test() {
+        let router = Router::new().route("/", post(handler));
+        let options = Options::builder()
+            .disable_redirect()
+            .socket_addr(([127, 0, 0, 1], 8080))
+            .build();
+
+        let server = WebhookServer::new(router, options);
+        server.run().await
+    }
+
+    async fn handler() -> &'static str {
+        println!("Test");
+        "Ok"
+    }
+}

From f1af892bef01b116f78365bcf27b7d60bfa255e2 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 12:44:25 +0100
Subject: [PATCH 07/31] Remove unused module

---
 stackable-webhook/src/lib.rs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 6a451c2a4..b3616c495 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -22,7 +22,6 @@ use tower::Service;
 use tracing::{debug, error, warn};
 
 pub mod constants;
-pub mod conversion;
 mod options;
 mod redirect;
 

From 67194d12a2c020f847560de7067f16dfd0e3ed06 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 13:14:03 +0100
Subject: [PATCH 08/31] Change hard-coded cert paths

---
 stackable-webhook/src/lib.rs | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index b3616c495..c1d06a4b3 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -97,10 +97,12 @@ impl WebhookServer {
             }
         }
 
-        let mut cert_file =
-            &mut BufReader::new(File::open("/tmp/webhook-certs/serverCert.pem").unwrap());
-        let mut key_file =
-            &mut BufReader::new(File::open("/tmp/webhook-certs/serverKey.pem").unwrap());
+        let mut cert_file = &mut BufReader::new(
+            File::open("/apiserver.local.config/certificates/apiserver.crt").unwrap(),
+        );
+        let mut key_file = &mut BufReader::new(
+            File::open("/apiserver.local.config/certificates/apiserver.key").unwrap(),
+        );
 
         let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0));
         let certs = certs(&mut cert_file)

From 2c3739f3bbd8f1aec2f6e52d337ada3d78eea18d Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 15:35:04 +0100
Subject: [PATCH 09/31] Start TLS server cleanup

---
 stackable-webhook/src/conversion.rs         |  42 -------
 stackable-webhook/src/lib.rs                | 122 +++-----------------
 stackable-webhook/src/servers/conversion.rs |  42 +++++++
 stackable-webhook/src/servers/mod.rs        |   0
 stackable-webhook/src/tls.rs                | 105 +++++++++++++++++
 5 files changed, 164 insertions(+), 147 deletions(-)
 delete mode 100644 stackable-webhook/src/conversion.rs
 create mode 100644 stackable-webhook/src/servers/conversion.rs
 create mode 100644 stackable-webhook/src/servers/mod.rs
 create mode 100644 stackable-webhook/src/tls.rs

diff --git a/stackable-webhook/src/conversion.rs b/stackable-webhook/src/conversion.rs
deleted file mode 100644
index 3f0ab8421..000000000
--- a/stackable-webhook/src/conversion.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use std::{net::SocketAddr, ops::Deref};
-
-use axum::{
-    routing::{post, MethodRouter},
-    Json,
-};
-use kube::core::conversion::{ConversionRequest, ConversionResponse};
-
-use crate::{Handlers, WebhookServer};
-
-pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>);
-
-impl Deref for ConversionWebhookServer {
-    type Target = WebhookServer<ConversionHandlers>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl ConversionWebhookServer {
-    pub async fn new(socket_addr: SocketAddr) -> Self {
-        Self(WebhookServer::new(socket_addr, ConversionHandlers).await)
-    }
-}
-
-pub struct ConversionHandlers;
-
-impl Handlers for ConversionHandlers {
-    fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)>
-    where
-        T: Clone + Sync + Send + 'static,
-    {
-        vec![("/convert", post(convert_handler))]
-    }
-}
-
-async fn convert_handler(
-    Json(_conversion_request): Json<ConversionRequest>,
-) -> Json<ConversionResponse> {
-    todo!()
-}
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index c1d06a4b3..d344f7b56 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -5,28 +5,19 @@
 //!
 //! The crate is also fully compatible with [`tracing`], and emits multiple
 //! levels of tracing data.
-
-use std::{fs::File, io::BufReader, sync::Arc};
-
-use axum::{extract::Request, Router};
-use futures_util::pin_mut;
-use hyper::body::Incoming;
-use hyper_util::rt::{TokioExecutor, TokioIo};
-use rustls_pemfile::{certs, pkcs8_private_keys};
-use tokio::net::TcpListener;
-use tokio_rustls::{
-    rustls::{Certificate, PrivateKey, ServerConfig},
-    TlsAcceptor,
-};
-use tower::Service;
-use tracing::{debug, error, warn};
+use axum::Router;
+use tracing::{debug, warn};
 
 pub mod constants;
+pub mod servers;
+
 mod options;
 mod redirect;
+mod tls;
 
 pub use options::*;
 pub use redirect::*;
+pub use tls::*;
 
 /// A ready-to-use webhook server.
 pub struct WebhookServer {
@@ -97,101 +88,22 @@ impl WebhookServer {
             }
         }
 
-        let mut cert_file = &mut BufReader::new(
-            File::open("/apiserver.local.config/certificates/apiserver.crt").unwrap(),
-        );
-        let mut key_file = &mut BufReader::new(
-            File::open("/apiserver.local.config/certificates/apiserver.key").unwrap(),
-        );
-
-        let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0));
-        let certs = certs(&mut cert_file)
-            .unwrap()
-            .into_iter()
-            .map(Certificate)
-            .collect();
-
-        let mut config = ServerConfig::builder()
-            .with_safe_defaults()
-            .with_no_client_auth()
-            .with_single_cert(certs, key)
-            .expect("bad certificate/key");
-
-        config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
-
-        let config = Arc::new(config);
-        let tls_acceptor = TlsAcceptor::from(config);
-
         // Create the root router and merge the provided router into it.
-        let mut app = Router::new();
-        app = app.merge(self.router);
-
-        let tcp_listener = TcpListener::bind(self.options.socket_addr).await.unwrap();
-        println!("Binded");
-
-        pin_mut!(tcp_listener);
-        loop {
-            let tower_service = app.clone();
-            let tls_acceptor = tls_acceptor.clone();
-
-            // Wait for new tcp connection
-            let (cnx, addr) = tcp_listener.accept().await.unwrap();
-            println!("TCP Accepted");
-
-            tokio::spawn(async move {
-                // Wait for tls handshake to happen
-                let Ok(stream) = tls_acceptor.accept(cnx).await else {
-                    error!("error during tls handshake connection from {}", addr);
-                    return;
-                };
-
-                println!("TLS Accepted");
-
-                // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio.
-                // `TokioIo` converts between them.
-                let stream = TokioIo::new(stream);
-
-                // Hyper also has its own `Service` trait and doesn't use tower. We can use
-                // `hyper::service::service_fn` to create a hyper `Service` that calls our app through
-                // `tower::Service::call`.
-                let hyper_service =
-                    hyper::service::service_fn(move |request: Request<Incoming>| {
-                        // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
-                        // tower's `Service` requires `&mut self`.
-                        //
-                        // We don't need to call `poll_ready` since `Router` is always ready.
-                        tower_service.clone().call(request)
-                    });
-
-                let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
-                    .serve_connection_with_upgrades(stream, hyper_service)
-                    .await;
-
-                if let Err(err) = ret {
-                    warn!("error serving connection from {}: {}", addr, err);
-                }
-            });
-        }
+        let mut router = Router::new();
+        router = router.merge(self.router);
+
+        // Create server for TLS termination
+        let tls_server = TlsServer::new(
+            self.options.socket_addr,
+            router,
+            "/apiserver.local.config/certificates/apiserver.crt",
+            "/apiserver.local.config/certificates/apiserver.key",
+        );
 
-        // axum::serve(listener, router).await.unwrap()
+        tls_server.run().await;
     }
 }
 
-pub struct TlsServer {
-    config: Arc<ServerConfig>,
-}
-
-impl TlsServer {
-    // pub fn new() -> Self {
-    //     let config = ServerConfig::builder()
-    //         .with_no_client_auth()
-    //         .with_cert_resolver(cert_resolver);
-    //     let config = Arc::new(config);
-
-    //     Self { config }
-    // }
-}
-
 #[cfg(test)]
 mod test {
     use super::*;
diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs
new file mode 100644
index 000000000..dd9ca4169
--- /dev/null
+++ b/stackable-webhook/src/servers/conversion.rs
@@ -0,0 +1,42 @@
+// use std::{net::SocketAddr, ops::Deref};
+
+// use axum::{
+//     routing::{post, MethodRouter},
+//     Json,
+// };
+// use kube::core::conversion::{ConversionRequest, ConversionResponse};
+
+// use crate::{Handlers, WebhookServer};
+
+// pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>);
+
+// impl Deref for ConversionWebhookServer {
+//     type Target = WebhookServer<ConversionHandlers>;
+
+//     fn deref(&self) -> &Self::Target {
+//         &self.0
+//     }
+// }
+
+// impl ConversionWebhookServer {
+//     pub async fn new(socket_addr: SocketAddr) -> Self {
+//         Self(WebhookServer::new(socket_addr, ConversionHandlers).await)
+//     }
+// }
+
+// pub struct ConversionHandlers;
+
+// impl Handlers for ConversionHandlers {
+//     fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)>
+//     where
+//         T: Clone + Sync + Send + 'static,
+//     {
+//         vec![("/convert", post(convert_handler))]
+//     }
+// }
+
+// async fn convert_handler(
+//     Json(_conversion_request): Json<ConversionRequest>,
+// ) -> Json<ConversionResponse> {
+//     todo!()
+// }
diff --git a/stackable-webhook/src/servers/mod.rs b/stackable-webhook/src/servers/mod.rs
new file mode 100644
index 000000000..e69de29bb
diff --git a/stackable-webhook/src/tls.rs b/stackable-webhook/src/tls.rs
new file mode 100644
index 000000000..a6d0bd9d3
--- /dev/null
+++ b/stackable-webhook/src/tls.rs
@@ -0,0 +1,105 @@
+use std::{fs::File, io::BufReader, net::SocketAddr, path::Path, sync::Arc};
+
+use axum::{extract::Request, Router};
+use futures_util::pin_mut;
+use hyper::body::Incoming;
+use hyper_util::rt::{TokioExecutor, TokioIo};
+use rustls_pemfile::{certs, pkcs8_private_keys};
+use tokio::net::TcpListener;
+use tokio_rustls::{
+    rustls::{Certificate, PrivateKey, ServerConfig},
+    TlsAcceptor,
+};
+use tower::Service;
+use tracing::{error, warn};
+
+pub struct TlsServer {
+    config: Arc<ServerConfig>,
+    socket_addr: SocketAddr,
+    router: Router,
+}
+
+impl TlsServer {
+    pub fn new(
+        socket_addr: SocketAddr,
+        router: Router,
+        cert_file: impl AsRef<Path>,
+        key_file: impl AsRef<Path>,
+    ) -> Self {
+        // TODO (@Techassi): Abstract away the cert chain loading
+        // TODO (@Techassi): Remove unwraps
+        let mut cert_file = &mut BufReader::new(File::open(cert_file).unwrap());
+        let mut key_file = &mut BufReader::new(File::open(key_file).unwrap());
+
+        // TODO (@Techassi): Remove unwrap
+        let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0));
+        let certs = certs(&mut cert_file)
+            .unwrap()
+            .into_iter()
+            .map(Certificate)
+            .collect();
+
+        // TODO (@Techassi): Use the latest version of rustls related crates
+        // TODO (@Techassi): Remove expect
+        let mut config = ServerConfig::builder()
+            .with_safe_defaults()
+            .with_no_client_auth()
+            .with_single_cert(certs, key)
+            .expect("bad certificate/key");
+
+        config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+        let config = Arc::new(config);
+
+        Self {
+            socket_addr,
+            config,
+            router,
+        }
+    }
+
+    pub async fn run(self) {
+        // TODO (@Techassi): Remove unwrap
+        let tls_acceptor = TlsAcceptor::from(self.config);
+        let tcp_listener = TcpListener::bind(self.socket_addr).await.unwrap();
+
+        pin_mut!(tcp_listener);
+        loop {
+            let tls_acceptor = tls_acceptor.clone();
+            let router = self.router.clone();
+
+            // Wait for new tcp connection
+            let (tcp_stream, remote_addr) = tcp_listener.accept().await.unwrap();
+
+            tokio::spawn(async move {
+                // Wait for tls handshake to happen
+                let Ok(tls_stream) = tls_acceptor.accept(tcp_stream).await else {
+                    error!("error during tls handshake connection from {}", remote_addr);
+                    return;
+                };
+
+                // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio.
+                // `TokioIo` converts between them.
+                let tls_stream = TokioIo::new(tls_stream);
+
+                // Hyper also has its own `Service` trait and doesn't use tower. We can use
+                // `hyper::service::service_fn` to create a hyper `Service` that calls our app through
+                // `tower::Service::call`.
+                let hyper_service =
+                    hyper::service::service_fn(move |request: Request<Incoming>| {
+                        // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
+                        // tower's `Service` requires `&mut self`.
+                        //
+                        // We don't need to call `poll_ready` since `Router` is always ready.
+                        router.clone().call(request)
+                    });
+
+                if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
+                    .serve_connection_with_upgrades(tls_stream, hyper_service)
+                    .await
+                {
+                    warn!(%err, "failed to serve connection from {}", remote_addr);
+                }
+            });
+        }
+    }
+}

From d00d38c1b8291a558bc146faf287c10a1f9cd6d4 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 16:35:20 +0100
Subject: [PATCH 10/31] Move code, abstract away certificate chain

---
 stackable-webhook/src/options.rs              | 12 ++-
 stackable-webhook/src/redirect.rs             | 10 ++-
 stackable-webhook/src/tls/certs.rs            | 76 +++++++++++++++++++
 stackable-webhook/src/tls/mod.rs              |  7 ++
 .../src/{tls.rs => tls/server.rs}             | 49 +++++-------
 5 files changed, 118 insertions(+), 36 deletions(-)
 create mode 100644 stackable-webhook/src/tls/certs.rs
 create mode 100644 stackable-webhook/src/tls/mod.rs
 rename stackable-webhook/src/{tls.rs => tls/server.rs} (66%)

diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 1a665d748..dcb5f4f97 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -1,4 +1,4 @@
-use std::net::SocketAddr;
+use std::{net::SocketAddr, path::PathBuf};
 
 use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS};
 
@@ -14,8 +14,9 @@ pub struct Options {
     /// it is required to specify the HTTP port.
     pub redirect: RedirectOption,
 
-    /// The default HTTPS socket address the [`TcpListener`] binds to. The same
-    /// IP adress is used for the auto HTTP to HTTPS redirect handler.
+    /// The default HTTPS socket address the [`TcpListener`][tokio::net::TcpListener]
+    /// binds to. The same IP adress is used for the auto HTTP to HTTPS redirect
+    /// handler.
     pub socket_addr: SocketAddr,
 
     /// Either auto-generate or use an injected TLS certificate.
@@ -94,7 +95,10 @@ impl Default for RedirectOption {
 #[derive(Debug)]
 pub enum TlsOption {
     AutoGenerate,
-    Inject,
+    Mount {
+        cert_path: PathBuf,
+        key_path: PathBuf,
+    },
 }
 
 impl Default for TlsOption {
diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs
index 1774bc2b0..0823fd4f5 100644
--- a/stackable-webhook/src/redirect.rs
+++ b/stackable-webhook/src/redirect.rs
@@ -22,12 +22,14 @@ pub enum Error {
     ConvertPartsToUri { source: InvalidUriParts },
 }
 
-/// A redirector which redirects HTTP connections at "/" to HTTPS automatically.
+/// A redirector which redirects all incoming HTTP connections to HTTPS
+/// automatically.
 ///
 /// Internally it uses a simple handler function which is registered as a
-/// singular [`Service`][tower::MakeService] at the root "/" path. If the
-/// conversion from HTTP to HTTPS fails, the [`Redirector`] returns a HTTP
-/// status code 400 (Bad Request). Additionally, a warning trace is emitted.
+/// singular [`Service`][tower::MakeService] at the root "/" path. The request
+/// paths are preserved. If the conversion from HTTP to HTTPS fails, the
+/// [`Redirector`] returns a HTTP status code 400 (Bad Request). Additionally,
+/// a warning trace is emitted.
 #[derive(Debug)]
 pub struct Redirector {
     ip_addr: IpAddr,
diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs
new file mode 100644
index 000000000..b4b147b67
--- /dev/null
+++ b/stackable-webhook/src/tls/certs.rs
@@ -0,0 +1,76 @@
+use std::{fs::File, io::BufReader, path::Path};
+
+use rustls_pemfile::{certs, pkcs8_private_keys};
+use snafu::{ResultExt, Snafu};
+use tokio_rustls::rustls::{Certificate, PrivateKey};
+
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("failed to read certificate file"))]
+    ReadCertFile { source: std::io::Error },
+
+    #[snafu(display("failed to read buffered certificate file"))]
+    ReadBufferedCertFile { source: std::io::Error },
+
+    #[snafu(display("failed to read private key file"))]
+    ReadKeyFile { source: std::io::Error },
+
+    #[snafu(display("failed to read buffered private key file"))]
+    ReadBufferedKeyFile { source: std::io::Error },
+}
+
+pub struct CertificateChain {
+    chain: Vec<Certificate>,
+    private_key: PrivateKey,
+}
+
+impl<C, P> TryFrom<(&mut C, &mut P)> for CertificateChain
+where
+    C: std::io::BufRead,
+    P: std::io::BufRead,
+{
+    type Error = Error;
+
+    fn try_from(readers: (&mut C, &mut P)) -> Result<Self, Self::Error> {
+        let chain = certs(readers.0)
+            .context(ReadBufferedCertFileSnafu)?
+            .into_iter()
+            .map(Certificate)
+            .collect();
+
+        let private_key = pkcs8_private_keys(readers.1)
+            .context(ReadBufferedKeyFileSnafu)?
+            .remove(0);
+        let private_key = PrivateKey(private_key);
+
+        Ok(Self { chain, private_key })
+    }
+}
+
+impl CertificateChain {
+    pub fn from_files<C, P>(certificate_path: C, private_key_path: P) -> Result<Self, Error>
+    where
+        C: AsRef<Path>,
+        P: AsRef<Path>,
+    {
+        let cert_file = File::open(certificate_path).context(ReadCertFileSnafu)?;
+        let cert_reader = &mut BufReader::new(cert_file);
+
+        let key_file = File::open(private_key_path).context(ReadKeyFileSnafu)?;
+        let key_reader = &mut BufReader::new(key_file);
+
+        Self::try_from((cert_reader, key_reader))
+    }
+
+    pub fn chain(&self) -> &[Certificate] {
+        &self.chain
+    }
+
+    pub fn private_key(&self) -> &PrivateKey {
+        &self.private_key
+    }
+
+    pub fn into_parts(self) -> (Vec<Certificate>, PrivateKey) {
+        (self.chain, self.private_key)
+    }
+}
diff --git a/stackable-webhook/src/tls/mod.rs b/stackable-webhook/src/tls/mod.rs
new file mode 100644
index 000000000..0d35fe57a
--- /dev/null
+++ b/stackable-webhook/src/tls/mod.rs
@@ -0,0 +1,7 @@
+//! This module contains structs and functions to easily create a TLS termination
+//! server, which can be used in combination with an Axum [`Router`].
+mod certs;
+mod server;
+
+pub use certs::*;
+pub use server::*;
diff --git a/stackable-webhook/src/tls.rs b/stackable-webhook/src/tls/server.rs
similarity index 66%
rename from stackable-webhook/src/tls.rs
rename to stackable-webhook/src/tls/server.rs
index a6d0bd9d3..6549f18f7 100644
--- a/stackable-webhook/src/tls.rs
+++ b/stackable-webhook/src/tls/server.rs
@@ -1,18 +1,20 @@
-use std::{fs::File, io::BufReader, net::SocketAddr, path::Path, sync::Arc};
+//! This module contains structs and functions to easily create a TLS termination
+//! server, which can be used in combination with an Axum [`Router`].
+use std::{net::SocketAddr, path::Path, sync::Arc};
 
 use axum::{extract::Request, Router};
 use futures_util::pin_mut;
-use hyper::body::Incoming;
+use hyper::{body::Incoming, service::service_fn};
 use hyper_util::rt::{TokioExecutor, TokioIo};
-use rustls_pemfile::{certs, pkcs8_private_keys};
 use tokio::net::TcpListener;
-use tokio_rustls::{
-    rustls::{Certificate, PrivateKey, ServerConfig},
-    TlsAcceptor,
-};
+use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
 use tower::Service;
 use tracing::{error, warn};
 
+use crate::CertificateChain;
+
+/// A server which terminates TLS connections and allows clients to commnunicate
+/// via HTTPS with the underlying HTTP router.
 pub struct TlsServer {
     config: Arc<ServerConfig>,
     socket_addr: SocketAddr,
@@ -23,28 +25,20 @@ impl TlsServer {
     pub fn new(
         socket_addr: SocketAddr,
         router: Router,
-        cert_file: impl AsRef<Path>,
-        key_file: impl AsRef<Path>,
+        cert_path: impl AsRef<Path>,
+        key_path: impl AsRef<Path>,
     ) -> Self {
-        // TODO (@Techassi): Abstract away the cert chain loading
-        // TODO (@Techassi): Remove unwraps
-        let mut cert_file = &mut BufReader::new(File::open(cert_file).unwrap());
-        let mut key_file = &mut BufReader::new(File::open(key_file).unwrap());
-
         // TODO (@Techassi): Remove unwrap
-        let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0));
-        let certs = certs(&mut cert_file)
+        let (chain, private_key) = CertificateChain::from_files(cert_path, key_path)
             .unwrap()
-            .into_iter()
-            .map(Certificate)
-            .collect();
+            .into_parts();
 
         // TODO (@Techassi): Use the latest version of rustls related crates
         // TODO (@Techassi): Remove expect
         let mut config = ServerConfig::builder()
             .with_safe_defaults()
             .with_no_client_auth()
-            .with_single_cert(certs, key)
+            .with_single_cert(chain, private_key)
             .expect("bad certificate/key");
 
         config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
@@ -84,14 +78,13 @@ impl TlsServer {
                 // Hyper also has its own `Service` trait and doesn't use tower. We can use
                 // `hyper::service::service_fn` to create a hyper `Service` that calls our app through
                 // `tower::Service::call`.
-                let hyper_service =
-                    hyper::service::service_fn(move |request: Request<Incoming>| {
-                        // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
-                        // tower's `Service` requires `&mut self`.
-                        //
-                        // We don't need to call `poll_ready` since `Router` is always ready.
-                        router.clone().call(request)
-                    });
+                let hyper_service = service_fn(move |request: Request<Incoming>| {
+                    // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
+                    // tower's `Service` requires `&mut self`.
+                    //
+                    // We don't need to call `poll_ready` since `Router` is always ready.
+                    router.clone().call(request)
+                });
 
                 if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
                     .serve_connection_with_upgrades(tls_stream, hyper_service)

From 96037fcad22426b3831841f9729a71d4ba9730f9 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 17:08:28 +0100
Subject: [PATCH 11/31] Change private key load function

---
 stackable-webhook/src/tls/certs.rs | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs
index b4b147b67..1dc819adc 100644
--- a/stackable-webhook/src/tls/certs.rs
+++ b/stackable-webhook/src/tls/certs.rs
@@ -1,6 +1,6 @@
 use std::{fs::File, io::BufReader, path::Path};
 
-use rustls_pemfile::{certs, pkcs8_private_keys};
+use rustls_pemfile::{certs, ec_private_keys};
 use snafu::{ResultExt, Snafu};
 use tokio_rustls::rustls::{Certificate, PrivateKey};
 
@@ -38,7 +38,8 @@ where
             .map(Certificate)
             .collect();
 
-        let private_key = pkcs8_private_keys(readers.1)
+        // TODO (@Techassi): Make this function configurable
+        let private_key = ec_private_keys(readers.1)
             .context(ReadBufferedKeyFileSnafu)?
             .remove(0);
         let private_key = PrivateKey(private_key);
@@ -74,3 +75,21 @@ impl CertificateChain {
         (self.chain, self.private_key)
     }
 }
+
+#[cfg(test)]
+mod test {
+
+    use rustls_pemfile::ec_private_keys;
+    use tokio_rustls::rustls::PrivateKey;
+
+    #[test]
+    fn test() {
+        let t = "-----BEGIN EC PRIVATE KEY-----\n
+MHcCAQEEIFMX2VakgYH6/5+aj7vinwmwVlBvTjCkw8/HjE3YE3xeoAoGCCqGSM49\n
+AwEHoUQDQgAE6lU4Z0tU8A+0jlwCFB1Efaq6nV+gbIDv1poXLf0d+wkMkiopOWlE\n
+QVYafabw9A/ziUVWTCovvuI7RWzA4l4Pqg==\n
+-----END EC PRIVATE KEY-----";
+        let key = ec_private_keys(&mut t.as_bytes()).unwrap().remove(0);
+        let key = PrivateKey(key);
+    }
+}

From c62d65c9d615cb596f7fa907e033f9efcbf75743 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 6 Feb 2024 17:17:58 +0100
Subject: [PATCH 12/31] Remove private key test

---
 stackable-webhook/src/tls/certs.rs | 18 ------------------
 1 file changed, 18 deletions(-)

diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs
index 1dc819adc..21c1255e6 100644
--- a/stackable-webhook/src/tls/certs.rs
+++ b/stackable-webhook/src/tls/certs.rs
@@ -75,21 +75,3 @@ impl CertificateChain {
         (self.chain, self.private_key)
     }
 }
-
-#[cfg(test)]
-mod test {
-
-    use rustls_pemfile::ec_private_keys;
-    use tokio_rustls::rustls::PrivateKey;
-
-    #[test]
-    fn test() {
-        let t = "-----BEGIN EC PRIVATE KEY-----\n
-MHcCAQEEIFMX2VakgYH6/5+aj7vinwmwVlBvTjCkw8/HjE3YE3xeoAoGCCqGSM49\n
-AwEHoUQDQgAE6lU4Z0tU8A+0jlwCFB1Efaq6nV+gbIDv1poXLf0d+wkMkiopOWlE\n
-QVYafabw9A/ziUVWTCovvuI7RWzA4l4Pqg==\n
------END EC PRIVATE KEY-----";
-        let key = ec_private_keys(&mut t.as_bytes()).unwrap().remove(0);
-        let key = PrivateKey(key);
-    }
-}

From 7bc7329e2bcb1193c6b7304b3aaeaaf5ec13b44c Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Thu, 8 Feb 2024 15:10:59 +0100
Subject: [PATCH 13/31] Add many doc comments, start bubbling up errors

---
 stackable-webhook/src/constants.rs  |   9 +-
 stackable-webhook/src/lib.rs        |  49 ++++++-----
 stackable-webhook/src/options.rs    | 123 +++++++++++++++++++++++-----
 stackable-webhook/src/tls/certs.rs  |   9 +-
 stackable-webhook/src/tls/server.rs |  93 ++++++++++++++-------
 5 files changed, 208 insertions(+), 75 deletions(-)

diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs
index b1c53a526..31fca207f 100644
--- a/stackable-webhook/src/constants.rs
+++ b/stackable-webhook/src/constants.rs
@@ -1,4 +1,7 @@
-pub const DEFAULT_HTTPS_PORT: u16 = 443;
-pub const DEFAULT_HTTP_PORT: u16 = 80;
+use std::net::{IpAddr, Ipv4Addr, SocketAddr};
 
-pub const DEFAULT_IP_ADDRESS: [u8; 4] = [127, 0, 0, 1];
+pub const DEFAULT_HTTPS_PORT: u16 = 8443;
+pub const DEFAULT_HTTP_PORT: u16 = 8080;
+
+pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
+pub const DEFAULT_SOCKET_ADDR: SocketAddr = SocketAddr::new(DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT);
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index d344f7b56..61dc7c597 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -6,20 +6,32 @@
 //! The crate is also fully compatible with [`tracing`], and emits multiple
 //! levels of tracing data.
 use axum::Router;
+use snafu::Snafu;
 use tracing::{debug, warn};
 
+use crate::{
+    options::{Options, RedirectOption},
+    redirect::Redirector,
+    tls::TlsServer,
+};
+
 pub mod constants;
+pub mod options;
+pub mod redirect;
 pub mod servers;
+pub mod tls;
 
-mod options;
-mod redirect;
-mod tls;
+pub type Result<T, E = Error> = std::result::Result<T, E>;
 
-pub use options::*;
-pub use redirect::*;
-pub use tls::*;
+#[derive(Debug, Snafu)]
+pub enum Error {}
 
 /// A ready-to-use webhook server.
+///
+/// This server abstracts away lower-level details like TLS termination
+/// and other various configurations, validations or middlewares. The routes
+/// and their handlers are completely customizable by bringing your own
+/// Axum [`Router`].
 pub struct WebhookServer {
     options: Options,
     router: Router,
@@ -93,13 +105,9 @@ impl WebhookServer {
         router = router.merge(self.router);
 
         // Create server for TLS termination
-        let tls_server = TlsServer::new(
-            self.options.socket_addr,
-            router,
-            "/apiserver.local.config/certificates/apiserver.crt",
-            "/apiserver.local.config/certificates/apiserver.key",
-        );
-
+        // TODO (@Techassi): Remove unwrap
+        let tls_server =
+            TlsServer::new(self.options.socket_addr, router, self.options.tls).unwrap();
         tls_server.run().await;
     }
 }
@@ -107,22 +115,19 @@ impl WebhookServer {
 #[cfg(test)]
 mod test {
     use super::*;
-    use axum::{routing::post, Router};
+    use axum::{routing::get, Router};
 
     #[tokio::test]
     async fn test() {
-        let router = Router::new().route("/", post(handler));
+        let router = Router::new().route("/", get(|| async { "Ok" }));
         let options = Options::builder()
-            .disable_redirect()
-            .socket_addr(([127, 0, 0, 1], 8080))
+            .tls_mount(
+                "/tmp/webhook-certs/serverCert.pem",
+                "/tmp/webhook-certs/serverKey.pem",
+            )
             .build();
 
         let server = WebhookServer::new(router, options);
         server.run().await
     }
-
-    async fn handler() -> &'static str {
-        println!("Test");
-        "Ok"
-    }
 }
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index dcb5f4f97..8b81b330c 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -1,6 +1,9 @@
-use std::{net::SocketAddr, path::PathBuf};
+use std::{
+    net::{IpAddr, SocketAddr},
+    path::PathBuf,
+};
 
-use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS};
+use crate::constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR};
 
 /// Specifies available webhook server options.
 ///
@@ -9,9 +12,58 @@ use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS
 ///
 /// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080
 /// - The socket binds to 127.0.0.1 on port 8443 (HTTPS)
+/// - The TLS cert used gets auto-generated
+///
+/// ### Example with Custom HTTPS IP Address and Port
+///
+/// ```
+/// use stackable_webhook::Options;
+///
+/// // Set IP address and port at the same time
+/// let options = Options::builder()
+///     .socket_addr([0, 0, 0, 0], 12345)
+///     .build();
+///
+/// // Set IP address only
+/// let options = Options::builder()
+///     .socket_ip([0, 0, 0, 0])
+///     .build();
+///
+/// // Set port only
+/// let options = Options::builder()
+///     .socket_port(12345)
+///     .build();
+/// ```
+///
+/// ### Example with Custom Redirects
+///
+/// ```
+/// use stackable_webhook::Options;
+///
+/// // Use a custom HTTP port
+/// let options = Options::builder()
+///     .enable_redirect(12345)
+///     .build();
+///
+/// // Disable auto-redirect
+/// let options = Options::builder()
+///     .disable_redirect()
+///     .build();
+/// ```
+///
+/// ### Example with Mounted TLS Certificate
+///
+/// ```
+/// use stackable_webhook::Options;
+///
+/// let options = Options::builder()
+///     .tls_mount("path/to/pem/cert", "path/to/pem/key")
+///     .build();
+/// ```
 pub struct Options {
     /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled,
-    /// it is required to specify the HTTP port.
+    /// it is required to specify the HTTP port. If disabled, the webhook
+    /// server **only** listens on HTTPS.
     pub redirect: RedirectOption,
 
     /// The default HTTPS socket address the [`TcpListener`][tokio::net::TcpListener]
@@ -30,11 +82,19 @@ impl Default for Options {
 }
 
 impl Options {
+    /// Returns the default [`OptionsBuilder`] which allows to selectively
+    /// customize the options. See the documention for [`Options`] for more
+    /// information on available functions.
     pub fn builder() -> OptionsBuilder {
         OptionsBuilder::default()
     }
 }
 
+/// The [`OptionsBuilder`] which allows to selectively customize the webhook
+/// server [`Options`].
+///
+/// Usually, this struct is not constructed manually, but instead by calling
+/// [`Options::builder()`] or [`OptionsBuilder::default()`].
 #[derive(Debug, Default)]
 pub struct OptionsBuilder {
     redirect: Option<RedirectOption>,
@@ -43,38 +103,63 @@ pub struct OptionsBuilder {
 }
 
 impl OptionsBuilder {
-    pub fn redirect(mut self, redirect: RedirectOption) -> Self {
-        self.redirect = Some(redirect);
+    /// Disables HTPP to HTTPS auto-redirect entirely. The webhook server
+    /// will only listen on HTTPS.
+    pub fn disable_redirect(mut self) -> Self {
+        self.redirect = Some(RedirectOption::Disabled);
+        self
+    }
+
+    /// Enables HTTP to HTTPS auto-redirect on `http_port`. The webhook
+    /// server will listen on both HTTP and HTTPS.
+    pub fn enable_redirect(mut self, http_port: u16) -> Self {
+        self.redirect = Some(RedirectOption::Enabled(http_port));
         self
     }
 
-    pub fn disable_redirect(self) -> Self {
-        self.redirect(RedirectOption::Disabled)
+    /// Sets the socket address the webhook server uses to bind for HTTPS.
+    pub fn socket_addr(mut self, socket_ip: impl Into<IpAddr>, socket_port: u16) -> Self {
+        self.socket_addr = Some(SocketAddr::new(socket_ip.into(), socket_port));
+        self
     }
 
-    pub fn enable_redirect(self, http_port: u16) -> Self {
-        self.redirect(RedirectOption::Enabled(http_port))
+    /// Sets the IP address of the socket address the webhook server uses to
+    /// bind for HTTPS.
+    pub fn socket_ip(mut self, socket_ip: impl Into<IpAddr>) -> Self {
+        let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR);
+        addr.set_ip(socket_ip.into());
+        self
+    }
+
+    /// Sets the port of the socket address the webhook server uses to bind
+    /// for HTTPS.
+    pub fn socket_port(mut self, socket_port: u16) -> Self {
+        let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR);
+        addr.set_port(socket_port);
+        self
     }
 
-    pub fn socket_addr<T>(mut self, socket_addr: T) -> Self
-    where
-        T: Into<SocketAddr>,
-    {
-        self.socket_addr = Some(socket_addr.into());
+    pub fn tls_autogenerate(mut self) -> Self {
+        self.tls = Some(TlsOption::AutoGenerate);
         self
     }
 
-    pub fn tls(mut self, tls: TlsOption) -> Self {
-        self.tls = Some(tls);
+    pub fn tls_mount(
+        mut self,
+        cert_path: impl Into<PathBuf>,
+        key_path: impl Into<PathBuf>,
+    ) -> Self {
+        self.tls = Some(TlsOption::Mount {
+            cert_path: cert_path.into(),
+            key_path: key_path.into(),
+        });
         self
     }
 
     pub fn build(self) -> Options {
         Options {
             redirect: self.redirect.unwrap_or_default(),
-            socket_addr: self
-                .socket_addr
-                .unwrap_or(SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT))),
+            socket_addr: self.socket_addr.unwrap_or(DEFAULT_SOCKET_ADDR),
             tls: self.tls.unwrap_or_default(),
         }
     }
diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs
index 21c1255e6..adc082869 100644
--- a/stackable-webhook/src/tls/certs.rs
+++ b/stackable-webhook/src/tls/certs.rs
@@ -5,7 +5,7 @@ use snafu::{ResultExt, Snafu};
 use tokio_rustls::rustls::{Certificate, PrivateKey};
 
 #[derive(Debug, Snafu)]
-pub enum Error {
+pub enum CertifacteError {
     #[snafu(display("failed to read certificate file"))]
     ReadCertFile { source: std::io::Error },
 
@@ -29,7 +29,7 @@ where
     C: std::io::BufRead,
     P: std::io::BufRead,
 {
-    type Error = Error;
+    type Error = CertifacteError;
 
     fn try_from(readers: (&mut C, &mut P)) -> Result<Self, Self::Error> {
         let chain = certs(readers.0)
@@ -49,7 +49,10 @@ where
 }
 
 impl CertificateChain {
-    pub fn from_files<C, P>(certificate_path: C, private_key_path: P) -> Result<Self, Error>
+    pub fn from_files<C, P>(
+        certificate_path: C,
+        private_key_path: P,
+    ) -> Result<Self, CertifacteError>
     where
         C: AsRef<Path>,
         P: AsRef<Path>,
diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs
index 6549f18f7..af5b30d7c 100644
--- a/stackable-webhook/src/tls/server.rs
+++ b/stackable-webhook/src/tls/server.rs
@@ -1,17 +1,29 @@
 //! This module contains structs and functions to easily create a TLS termination
 //! server, which can be used in combination with an Axum [`Router`].
-use std::{net::SocketAddr, path::Path, sync::Arc};
+use std::{net::SocketAddr, sync::Arc};
 
 use axum::{extract::Request, Router};
 use futures_util::pin_mut;
 use hyper::{body::Incoming, service::service_fn};
 use hyper_util::rt::{TokioExecutor, TokioIo};
+use snafu::Snafu;
 use tokio::net::TcpListener;
 use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
 use tower::Service;
 use tracing::{error, warn};
 
-use crate::CertificateChain;
+use crate::{
+    options::TlsOption,
+    tls::{CertifacteError, CertificateChain},
+};
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("failed to create TLS certificate chain"))]
+    TlsCertificateChain { source: CertifacteError },
+}
 
 /// A server which terminates TLS connections and allows clients to commnunicate
 /// via HTTPS with the underlying HTTP router.
@@ -22,35 +34,50 @@ pub struct TlsServer {
 }
 
 impl TlsServer {
-    pub fn new(
-        socket_addr: SocketAddr,
-        router: Router,
-        cert_path: impl AsRef<Path>,
-        key_path: impl AsRef<Path>,
-    ) -> Self {
-        // TODO (@Techassi): Remove unwrap
-        let (chain, private_key) = CertificateChain::from_files(cert_path, key_path)
-            .unwrap()
-            .into_parts();
-
-        // TODO (@Techassi): Use the latest version of rustls related crates
-        // TODO (@Techassi): Remove expect
-        let mut config = ServerConfig::builder()
-            .with_safe_defaults()
-            .with_no_client_auth()
-            .with_single_cert(chain, private_key)
-            .expect("bad certificate/key");
-
-        config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
-        let config = Arc::new(config);
-
-        Self {
+    pub fn new(socket_addr: SocketAddr, router: Router, tls: TlsOption) -> Result<Self> {
+        let config = match tls {
+            TlsOption::AutoGenerate => {
+                // let mut config = ServerConfig::builder()
+                //     .with_safe_defaults()
+                //     .with_no_client_auth()
+                //     .with_cert_resolver(cert_resolver);
+                // config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+                todo!()
+            }
+            TlsOption::Mount {
+                cert_path,
+                key_path,
+            } => {
+                // TODO (@Techassi): Remove unwrap
+                let (chain, private_key) = CertificateChain::from_files(cert_path, key_path)
+                    .unwrap()
+                    .into_parts();
+
+                // TODO (@Techassi): Use the latest version of rustls related crates
+                // TODO (@Techassi): Remove expect
+                let mut config = ServerConfig::builder()
+                    .with_safe_defaults()
+                    .with_no_client_auth()
+                    .with_single_cert(chain, private_key)
+                    .expect("bad certificate/key");
+
+                config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+                config
+            }
+        }
+        .wrap(Arc::new); // Silly little thing, drop it again I guess
+
+        Ok(Self {
             socket_addr,
             config,
             router,
-        }
+        })
     }
 
+    /// Runs the TLS server by listening for incoming TCP connections on the
+    /// bound socket address. It only accepts TLS connections. Internally each
+    /// TLS stream get handled by a Hyper service, which in turn is an Axum
+    /// router.
     pub async fn run(self) {
         // TODO (@Techassi): Remove unwrap
         let tls_acceptor = TlsAcceptor::from(self.config);
@@ -78,7 +105,7 @@ impl TlsServer {
                 // Hyper also has its own `Service` trait and doesn't use tower. We can use
                 // `hyper::service::service_fn` to create a hyper `Service` that calls our app through
                 // `tower::Service::call`.
-                let hyper_service = service_fn(move |request: Request<Incoming>| {
+                let service = service_fn(move |request: Request<Incoming>| {
                     // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
                     // tower's `Service` requires `&mut self`.
                     //
@@ -87,7 +114,7 @@ impl TlsServer {
                 });
 
                 if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
-                    .serve_connection_with_upgrades(tls_stream, hyper_service)
+                    .serve_connection_with_upgrades(tls_stream, service)
                     .await
                 {
                     warn!(%err, "failed to serve connection from {}", remote_addr);
@@ -96,3 +123,13 @@ impl TlsServer {
         }
     }
 }
+
+trait Wrap<S, T>: Sized {
+    fn wrap(self, f: impl Fn(S) -> T) -> T;
+}
+
+impl<S, T> Wrap<S, T> for S {
+    fn wrap(self, f: impl Fn(S) -> T) -> T {
+        f(self)
+    }
+}

From 05c2d65ceafe97abe119d4237ee574b4b9eb866c Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Fri, 9 Feb 2024 16:16:08 +0100
Subject: [PATCH 14/31] Add doc comments, continue error handling

---
 stackable-webhook/src/constants.rs  |  2 +
 stackable-webhook/src/lib.rs        | 68 +++++++++++++++++++++++------
 stackable-webhook/src/options.rs    | 10 +++++
 stackable-webhook/src/redirect.rs   |  1 +
 stackable-webhook/src/tls/mod.rs    |  4 +-
 stackable-webhook/src/tls/server.rs | 59 +++++++++++++++----------
 6 files changed, 105 insertions(+), 39 deletions(-)

diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs
index 31fca207f..d124133f8 100644
--- a/stackable-webhook/src/constants.rs
+++ b/stackable-webhook/src/constants.rs
@@ -1,3 +1,5 @@
+//! Contains various constant definitions, mostly for default ports and IP
+//! addresses.
 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
 
 pub const DEFAULT_HTTPS_PORT: u16 = 8443;
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 61dc7c597..a5732c140 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -1,13 +1,32 @@
 //! Utility types and functions to easily create ready-to-use webhook servers
 //! which can handle different tasks, for example CRD conversions. All webhook
-//! servers use HTTPS per default and provide options to enable HTTP to HTTPS
-//! redirection as well.
+//! servers use HTTPS by default and provides options to enable HTTP to HTTPS
+//! redirection as well. This library is fully compatible with the [`tracing`]
+//! crate and emits multiple levels of tracing data.
 //!
-//! The crate is also fully compatible with [`tracing`], and emits multiple
-//! levels of tracing data.
+//! Most users will only use the top-level exported generic [`WebhookServer`]
+//! which enables complete control over the [Router] which handles registering
+//! routes and their handler functions.
+//!
+//! ```
+//! use stackable_webhook::{WebhookServer, Options};
+//! use axum::Router;
+//!
+//! let router = Router::new();
+//! let server = WebhookServer::new(router, Options::default());
+//! ```
+//!
+//! For some application complete end-to-end [`WebhookServer`] implementations
+//! exist. One such implementation is the [`ConversionWebhookServer`][1]. The
+//! only required parameters are a conversion handler function and [`Options`].
+//!
+//! [1]: crate::servers::ConversionWebhookServer
+//!
+//! This library additionally also exposes lower-level structs and functions to
+//! enable complete controll over these details if needed.
 use axum::Router;
-use snafu::Snafu;
-use tracing::{debug, warn};
+use snafu::{ResultExt, Snafu};
+use tracing::{debug, info, instrument, warn};
 
 use crate::{
     options::{Options, RedirectOption},
@@ -23,8 +42,25 @@ pub mod tls;
 
 pub type Result<T, E = Error> = std::result::Result<T, E>;
 
+/// A generic webhook handler receiving a request and sending back a response.
+///
+/// This trait is usually not implemented by external callers and this library
+/// provides various ready-to-use implementations for it. One such an
+/// implementation is part of the [`ConversionWebhookServer`][1].
+///
+/// [1]: crate::servers::ConversionWebhookServer
+pub trait WebhookHandler<Req, Res> {
+    fn call(self, req: Res) -> Res;
+}
+
 #[derive(Debug, Snafu)]
-pub enum Error {}
+pub enum Error {
+    #[snafu(display("failed to create TLS server"))]
+    CreateTlsServer { source: tls::Error },
+
+    #[snafu(display("failed to run TLS server"))]
+    RunTlsServer { source: tls::Error },
+}
 
 /// A ready-to-use webhook server.
 ///
@@ -72,6 +108,7 @@ impl WebhookServer {
     /// let router = Router::new();
     /// let server = WebhookServer::new(router, options);
     /// ```
+    #[instrument(name = "create_webhook_server", skip(router))]
     pub fn new(router: Router, options: Options) -> Self {
         debug!("create new webhook server");
         Self { options, router }
@@ -79,7 +116,8 @@ impl WebhookServer {
 
     /// Runs the webhook server by creating a TCP listener and binding it to
     /// the specified socket address.
-    pub async fn run(self) {
+    #[instrument(name = "run_webhook_server", skip(self), fields(self.options))]
+    pub async fn run(self) -> Result<()> {
         debug!("run webhook server");
 
         // Only run the auto redirector when enabled
@@ -93,6 +131,7 @@ impl WebhookServer {
                     http_port,
                 );
 
+                info!(http_port, "spawning redirector in separate task");
                 tokio::spawn(redirector.run());
             }
             RedirectOption::Disabled => {
@@ -101,14 +140,17 @@ impl WebhookServer {
         }
 
         // Create the root router and merge the provided router into it.
+        debug!("create core couter and merge provided router");
         let mut router = Router::new();
         router = router.merge(self.router);
 
         // Create server for TLS termination
-        // TODO (@Techassi): Remove unwrap
-        let tls_server =
-            TlsServer::new(self.options.socket_addr, router, self.options.tls).unwrap();
-        tls_server.run().await;
+        debug!("create TLS server");
+        let tls_server = TlsServer::new(self.options.socket_addr, router, self.options.tls)
+            .context(CreateTlsServerSnafu)?;
+
+        info!("running TLS server");
+        tls_server.run().await.context(RunTlsServerSnafu)
     }
 }
 
@@ -128,6 +170,6 @@ mod test {
             .build();
 
         let server = WebhookServer::new(router, options);
-        server.run().await
+        server.run().await.unwrap()
     }
 }
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 8b81b330c..0687b05f3 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -1,3 +1,4 @@
+//! Contains available options to configure the [WebhookServer][crate::WebhookServer].
 use std::{
     net::{IpAddr, SocketAddr},
     path::PathBuf,
@@ -60,6 +61,7 @@ use crate::constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR};
 ///     .tls_mount("path/to/pem/cert", "path/to/pem/key")
 ///     .build();
 /// ```
+#[derive(Debug)]
 pub struct Options {
     /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled,
     /// it is required to specify the HTTP port. If disabled, the webhook
@@ -139,11 +141,17 @@ impl OptionsBuilder {
         self
     }
 
+    /// Enables TLS certificate auto-generation instead of using a mounted
+    /// one. If instead a mounted TLS certificate is needed, use the
+    /// [`OptionsBuilder::tls_mount()`] function.
     pub fn tls_autogenerate(mut self) -> Self {
         self.tls = Some(TlsOption::AutoGenerate);
         self
     }
 
+    /// Uses a mounted TLS certificate instead of auto-generating one. If
+    /// instead a auto-generated TLS certificate is needed, us ethe
+    /// [`OptionsBuilder::tls_autogenerate()`] function.
     pub fn tls_mount(
         mut self,
         cert_path: impl Into<PathBuf>,
@@ -156,6 +164,8 @@ impl OptionsBuilder {
         self
     }
 
+    /// Builds the final [`Options`] by using default values for any not
+    /// explicitly set option.
     pub fn build(self) -> Options {
         Options {
             redirect: self.redirect.unwrap_or_default(),
diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs
index 0823fd4f5..fb8b0b992 100644
--- a/stackable-webhook/src/redirect.rs
+++ b/stackable-webhook/src/redirect.rs
@@ -1,3 +1,4 @@
+//! Contains structs and functions to enable auto HTTP to HTTPS redirection.
 use std::net::{IpAddr, SocketAddr};
 
 use axum::{
diff --git a/stackable-webhook/src/tls/mod.rs b/stackable-webhook/src/tls/mod.rs
index 0d35fe57a..c180c65ec 100644
--- a/stackable-webhook/src/tls/mod.rs
+++ b/stackable-webhook/src/tls/mod.rs
@@ -1,5 +1,5 @@
-//! This module contains structs and functions to easily create a TLS termination
-//! server, which can be used in combination with an Axum [`Router`].
+//! Contains structs and functions to easily create a TLS termination server,
+//! which can be used in combination with an Axum [`Router`][axum::Router].
 mod certs;
 mod server;
 
diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs
index af5b30d7c..a9638a4db 100644
--- a/stackable-webhook/src/tls/server.rs
+++ b/stackable-webhook/src/tls/server.rs
@@ -6,11 +6,11 @@ use axum::{extract::Request, Router};
 use futures_util::pin_mut;
 use hyper::{body::Incoming, service::service_fn};
 use hyper_util::rt::{TokioExecutor, TokioIo};
-use snafu::Snafu;
+use snafu::{ResultExt, Snafu};
 use tokio::net::TcpListener;
 use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
 use tower::Service;
-use tracing::{error, warn};
+use tracing::{error, instrument, warn};
 
 use crate::{
     options::TlsOption,
@@ -23,6 +23,17 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
 pub enum Error {
     #[snafu(display("failed to create TLS certificate chain"))]
     TlsCertificateChain { source: CertifacteError },
+
+    #[snafu(display("failed to construct TLS server config, bad certificate/key"))]
+    InvalidTlsPrivateKey { source: tokio_rustls::rustls::Error },
+
+    #[snafu(display(
+        "failed to create TCP listener by binding to socket address {socket_addr:?}"
+    ))]
+    BindTcpListener {
+        source: std::io::Error,
+        socket_addr: SocketAddr,
+    },
 }
 
 /// A server which terminates TLS connections and allows clients to commnunicate
@@ -34,6 +45,7 @@ pub struct TlsServer {
 }
 
 impl TlsServer {
+    #[instrument(name = "create_tls_server", skip(router))]
     pub fn new(socket_addr: SocketAddr, router: Router, tls: TlsOption) -> Result<Self> {
         let config = match tls {
             TlsOption::AutoGenerate => {
@@ -48,24 +60,22 @@ impl TlsServer {
                 cert_path,
                 key_path,
             } => {
-                // TODO (@Techassi): Remove unwrap
                 let (chain, private_key) = CertificateChain::from_files(cert_path, key_path)
-                    .unwrap()
+                    .context(TlsCertificateChainSnafu)?
                     .into_parts();
 
                 // TODO (@Techassi): Use the latest version of rustls related crates
-                // TODO (@Techassi): Remove expect
                 let mut config = ServerConfig::builder()
                     .with_safe_defaults()
                     .with_no_client_auth()
                     .with_single_cert(chain, private_key)
-                    .expect("bad certificate/key");
+                    .context(InvalidTlsPrivateKeySnafu)?;
 
                 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
                 config
             }
-        }
-        .wrap(Arc::new); // Silly little thing, drop it again I guess
+        };
+        let config = Arc::new(config);
 
         Ok(Self {
             socket_addr,
@@ -78,10 +88,15 @@ impl TlsServer {
     /// bound socket address. It only accepts TLS connections. Internally each
     /// TLS stream get handled by a Hyper service, which in turn is an Axum
     /// router.
-    pub async fn run(self) {
-        // TODO (@Techassi): Remove unwrap
+    #[instrument(name = "run_tls_server", skip(self), fields(self.socket_addr, self.config))]
+    pub async fn run(self) -> Result<()> {
         let tls_acceptor = TlsAcceptor::from(self.config);
-        let tcp_listener = TcpListener::bind(self.socket_addr).await.unwrap();
+        let tcp_listener =
+            TcpListener::bind(self.socket_addr)
+                .await
+                .context(BindTcpListenerSnafu {
+                    socket_addr: self.socket_addr,
+                })?;
 
         pin_mut!(tcp_listener);
         loop {
@@ -89,12 +104,18 @@ impl TlsServer {
             let router = self.router.clone();
 
             // Wait for new tcp connection
-            let (tcp_stream, remote_addr) = tcp_listener.accept().await.unwrap();
+            let (tcp_stream, remote_addr) = match tcp_listener.accept().await {
+                Ok((stream, addr)) => (stream, addr),
+                Err(err) => {
+                    warn!(%err, "failed to accept incoming TCP connection");
+                    continue;
+                }
+            };
 
             tokio::spawn(async move {
                 // Wait for tls handshake to happen
                 let Ok(tls_stream) = tls_acceptor.accept(tcp_stream).await else {
-                    error!("error during tls handshake connection from {}", remote_addr);
+                    error!(%remote_addr, "error during tls handshake connection");
                     return;
                 };
 
@@ -117,19 +138,9 @@ impl TlsServer {
                     .serve_connection_with_upgrades(tls_stream, service)
                     .await
                 {
-                    warn!(%err, "failed to serve connection from {}", remote_addr);
+                    warn!(%err, %remote_addr, "failed to serve connection");
                 }
             });
         }
     }
 }
-
-trait Wrap<S, T>: Sized {
-    fn wrap(self, f: impl Fn(S) -> T) -> T;
-}
-
-impl<S, T> Wrap<S, T> for S {
-    fn wrap(self, f: impl Fn(S) -> T) -> T {
-        f(self)
-    }
-}

From 00f5a8b9da546a47a630406642e6953eeda43b02 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Fri, 9 Feb 2024 16:16:31 +0100
Subject: [PATCH 15/31] Add ready-to-use conversion webhook server

---
 stackable-webhook/src/servers/conversion.rs | 97 ++++++++++++---------
 stackable-webhook/src/servers/mod.rs        |  5 ++
 2 files changed, 60 insertions(+), 42 deletions(-)

diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs
index dd9ca4169..3366cca1c 100644
--- a/stackable-webhook/src/servers/conversion.rs
+++ b/stackable-webhook/src/servers/conversion.rs
@@ -1,42 +1,55 @@
-// use std::{net::SocketAddr, ops::Deref};
-
-// use axum::{
-//     routing::{post, MethodRouter},
-//     Json,
-// };
-// use kube::core::conversion::{ConversionRequest, ConversionResponse};
-
-// use crate::{Handlers, WebhookServer};
-
-// pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>);
-
-// impl Deref for ConversionWebhookServer {
-//     type Target = WebhookServer<ConversionHandlers>;
-
-//     fn deref(&self) -> &Self::Target {
-//         &self.0
-//     }
-// }
-
-// impl ConversionWebhookServer {
-//     pub async fn new(socket_addr: SocketAddr) -> Self {
-//         Self(WebhookServer::new(socket_addr, ConversionHandlers).await)
-//     }
-// }
-
-// pub struct ConversionHandlers;
-
-// impl Handlers for ConversionHandlers {
-//     fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)>
-//     where
-//         T: Clone + Sync + Send + 'static,
-//     {
-//         vec![("/convert", post(convert_handler))]
-//     }
-// }
-
-// async fn convert_handler(
-//     Json(_conversion_request): Json<ConversionRequest>,
-// ) -> Json<ConversionResponse> {
-//     todo!()
-// }
+use axum::{routing::post, Json, Router};
+use kube::core::conversion::ConversionReview;
+
+use crate::{options::Options, WebhookHandler, WebhookServer};
+
+impl<F> WebhookHandler<ConversionReview, ConversionReview> for F
+where
+    F: FnOnce(ConversionReview) -> ConversionReview,
+{
+    fn call(self, req: ConversionReview) -> ConversionReview {
+        self(req)
+    }
+}
+
+pub struct ConversionWebhookServer {
+    options: Options,
+    router: Router,
+}
+
+impl ConversionWebhookServer {
+    pub fn new<T>(handler: T, options: Options) -> Self
+    where
+        T: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static,
+    {
+        let handler_fn = |Json(review): Json<ConversionReview>| async {
+            let review = handler.call(review);
+            Json(review)
+        };
+
+        let router = Router::new().route("/convert", post(handler_fn));
+        Self { router, options }
+    }
+
+    pub async fn run(self) -> Result<(), crate::Error> {
+        let server = WebhookServer::new(self.router, self.options);
+        server.run().await
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::Options;
+
+    fn handler(req: ConversionReview) -> ConversionReview {
+        // In here we can do the CRD conversion
+        req
+    }
+
+    #[tokio::test]
+    async fn test() {
+        let server = ConversionWebhookServer::new(handler, Options::default());
+        server.run().await.unwrap();
+    }
+}
diff --git a/stackable-webhook/src/servers/mod.rs b/stackable-webhook/src/servers/mod.rs
index e69de29bb..b242df779 100644
--- a/stackable-webhook/src/servers/mod.rs
+++ b/stackable-webhook/src/servers/mod.rs
@@ -0,0 +1,5 @@
+//! Contains high-level ready-to-use webhook server implementations for specific
+//! purposes.
+mod conversion;
+
+pub use conversion::*;

From 69714e84e11ef720a13dee15bfae63c44b3331d5 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Mon, 12 Feb 2024 11:48:44 +0100
Subject: [PATCH 16/31] Add support for state in ready-to-use conversion
 webhook server

---
 stackable-webhook/src/lib.rs                |  4 ++
 stackable-webhook/src/servers/conversion.rs | 80 ++++++++++++++++++++-
 2 files changed, 81 insertions(+), 3 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index a5732c140..1830498c6 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -53,6 +53,10 @@ pub trait WebhookHandler<Req, Res> {
     fn call(self, req: Res) -> Res;
 }
 
+pub trait StatefulWebhookHandler<Req, Res, S> {
+    fn call(self, req: Res, state: S) -> Res;
+}
+
 #[derive(Debug, Snafu)]
 pub enum Error {
     #[snafu(display("failed to create TLS server"))]
diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs
index 3366cca1c..68dd85ef0 100644
--- a/stackable-webhook/src/servers/conversion.rs
+++ b/stackable-webhook/src/servers/conversion.rs
@@ -1,7 +1,7 @@
-use axum::{routing::post, Json, Router};
+use axum::{extract::State, routing::post, Json, Router};
 use kube::core::conversion::ConversionReview;
 
-use crate::{options::Options, WebhookHandler, WebhookServer};
+use crate::{options::Options, StatefulWebhookHandler, WebhookHandler, WebhookServer};
 
 impl<F> WebhookHandler<ConversionReview, ConversionReview> for F
 where
@@ -12,12 +12,27 @@ where
     }
 }
 
+impl<F, S> StatefulWebhookHandler<ConversionReview, ConversionReview, S> for F
+where
+    F: FnOnce(ConversionReview, S) -> ConversionReview,
+{
+    fn call(self, req: ConversionReview, state: S) -> ConversionReview {
+        self(req, state)
+    }
+}
+
 pub struct ConversionWebhookServer {
     options: Options,
     router: Router,
 }
 
 impl ConversionWebhookServer {
+    /// Creates a new conversion webhook server **without** state which expects
+    /// POST requests being made to the `/convert` endpoint.
+    ///
+    /// Each request is handled by the provided `handler` function. Any function
+    /// with the signature `(ConversionReview) -> ConversionReview` can be
+    /// provided.
     pub fn new<T>(handler: T, options: Options) -> Self
     where
         T: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static,
@@ -28,6 +43,42 @@ impl ConversionWebhookServer {
         };
 
         let router = Router::new().route("/convert", post(handler_fn));
+
+        Self { router, options }
+    }
+
+    /// Creates a new conversion webhook server **with** state which expects
+    /// POST requests being made to the `/convert` endpoint.
+    ///
+    /// Each request is handled by the provided `handler` function. Any function
+    /// with the signature `(ConversionReview, S) -> ConversionReview` can be
+    /// provided.
+    ///
+    /// It is recommended to wrap the state in an [`Arc`][std::sync::Arc] if it
+    /// needs to be mutable.
+    ///
+    /// ### See
+    ///
+    /// - <https://docs.rs/axum/latest/axum/index.html#sharing-state-with-handlers>
+    pub fn new_with_state<T, S>(handler: T, state: S, options: Options) -> Self
+    where
+        T: StatefulWebhookHandler<ConversionReview, ConversionReview, S>
+            + Clone
+            + Send
+            + Sync
+            + 'static,
+        S: Clone + Send + Sync + 'static,
+    {
+        // See https://github.com/async-graphql/async-graphql/discussions/1150
+        let handler_fn = |State(state): State<S>, Json(review): Json<ConversionReview>| async {
+            let review = handler.call(review, state);
+            Json(review)
+        };
+
+        let router = Router::new()
+            .route("/convert", post(handler_fn))
+            .with_state(state);
+
         Self { router, options }
     }
 
@@ -39,17 +90,40 @@ impl ConversionWebhookServer {
 
 #[cfg(test)]
 mod test {
+    use std::sync::Arc;
+
     use super::*;
     use crate::Options;
 
+    #[derive(Debug, Clone)]
+    struct State {
+        inner: usize,
+    }
+
     fn handler(req: ConversionReview) -> ConversionReview {
         // In here we can do the CRD conversion
         req
     }
 
+    fn handler_with_state(req: ConversionReview, state: Arc<State>) -> ConversionReview {
+        println!("{}", state.inner);
+        req
+    }
+
     #[tokio::test]
-    async fn test() {
+    async fn without_state() {
         let server = ConversionWebhookServer::new(handler, Options::default());
         server.run().await.unwrap();
     }
+
+    #[tokio::test]
+    async fn with_state() {
+        let shared_state = Arc::new(State { inner: 0 });
+        let server = ConversionWebhookServer::new_with_state(
+            handler_with_state,
+            shared_state,
+            Options::default(),
+        );
+        server.run().await.unwrap();
+    }
 }

From d4db28d019b95404331d8816f79ede8d8518c2f3 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Mon, 12 Feb 2024 13:17:48 +0100
Subject: [PATCH 17/31] Adjust doc comments

---
 stackable-webhook/src/lib.rs                | 33 +++++----
 stackable-webhook/src/servers/conversion.rs | 82 ++++++++++++++++++---
 2 files changed, 91 insertions(+), 24 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 1830498c6..a09a02566 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -16,23 +16,17 @@
 //! let server = WebhookServer::new(router, Options::default());
 //! ```
 //!
-//! For some application complete end-to-end [`WebhookServer`] implementations
-//! exist. One such implementation is the [`ConversionWebhookServer`][1]. The
+//! For some usages, complete end-to-end [`WebhookServer`] implementations
+//! exist. One such implementation is the [`ConversionWebhookServer`]. The
 //! only required parameters are a conversion handler function and [`Options`].
 //!
-//! [1]: crate::servers::ConversionWebhookServer
-//!
 //! This library additionally also exposes lower-level structs and functions to
 //! enable complete controll over these details if needed.
 use axum::Router;
 use snafu::{ResultExt, Snafu};
 use tracing::{debug, info, instrument, warn};
 
-use crate::{
-    options::{Options, RedirectOption},
-    redirect::Redirector,
-    tls::TlsServer,
-};
+use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer};
 
 pub mod constants;
 pub mod options;
@@ -40,21 +34,29 @@ pub mod redirect;
 pub mod servers;
 pub mod tls;
 
+// Selected re-exports
+pub use crate::{options::Options, servers::ConversionWebhookServer};
+
+/// A result type alias with the library-level [`Error`] type as teh default
+/// error type.
 pub type Result<T, E = Error> = std::result::Result<T, E>;
 
 /// A generic webhook handler receiving a request and sending back a response.
 ///
 /// This trait is usually not implemented by external callers and this library
 /// provides various ready-to-use implementations for it. One such an
-/// implementation is part of the [`ConversionWebhookServer`][1].
-///
-/// [1]: crate::servers::ConversionWebhookServer
 pub trait WebhookHandler<Req, Res> {
-    fn call(self, req: Res) -> Res;
+    fn call(self, req: Req) -> Res;
 }
 
+/// A generic webhook handler receiving a request and state and sending back
+/// a response.
+///
+/// This trait is usually not implemented by external callers and this library
+/// provides various ready-to-use implementations for it. One such an
+/// implementation is part of the [`ConversionWebhookServer`].
 pub trait StatefulWebhookHandler<Req, Res, S> {
-    fn call(self, req: Res, state: S) -> Res;
+    fn call(self, req: Req, state: S) -> Res;
 }
 
 #[derive(Debug, Snafu)]
@@ -72,6 +74,9 @@ pub enum Error {
 /// and other various configurations, validations or middlewares. The routes
 /// and their handlers are completely customizable by bringing your own
 /// Axum [`Router`].
+///
+/// For complete complete end-to-end implementations, see
+/// [`ConversionWebhookServer`].
 pub struct WebhookServer {
     options: Options,
     router: Router,
diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs
index 68dd85ef0..efa2f1f64 100644
--- a/stackable-webhook/src/servers/conversion.rs
+++ b/stackable-webhook/src/servers/conversion.rs
@@ -1,5 +1,8 @@
+use std::fmt::Debug;
+
 use axum::{extract::State, routing::post, Json, Router};
 use kube::core::conversion::ConversionReview;
+use tracing::{debug, instrument};
 
 use crate::{options::Options, StatefulWebhookHandler, WebhookHandler, WebhookServer};
 
@@ -21,6 +24,10 @@ where
     }
 }
 
+/// A ready-to-use CRD conversion webhook server.
+///
+/// See [`ConversionWebhookServer::new()`] and [`ConversionWebhookServer::new_with_state()`]
+/// for usage examples.
 pub struct ConversionWebhookServer {
     options: Options,
     router: Router,
@@ -33,17 +40,35 @@ impl ConversionWebhookServer {
     /// Each request is handled by the provided `handler` function. Any function
     /// with the signature `(ConversionReview) -> ConversionReview` can be
     /// provided.
-    pub fn new<T>(handler: T, options: Options) -> Self
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use stackable_webhook::{servers::ConversionWebhookServer, Options};
+    /// use kube::core::conversion::ConversionReview;
+    ///
+    /// // Construct the conversion webhook server
+    /// let server = ConversionWebhookServer::new(handler, Options::default());
+    ///
+    /// // Define the handler function
+    /// fn handler(req: ConversionReview) -> ConversionReview {
+    ///    // In here we can do the CRD conversion
+    ///    req
+    /// }
+    /// ```
+    #[instrument(name = "create_conversion_webhhok_server", skip(handler))]
+    pub fn new<H>(handler: H, options: Options) -> Self
     where
-        T: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static,
+        H: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static,
     {
+        debug!("create new conversion webhook server");
+
         let handler_fn = |Json(review): Json<ConversionReview>| async {
             let review = handler.call(review);
             Json(review)
         };
 
         let router = Router::new().route("/convert", post(handler_fn));
-
         Self { router, options }
     }
 
@@ -55,26 +80,58 @@ impl ConversionWebhookServer {
     /// provided.
     ///
     /// It is recommended to wrap the state in an [`Arc`][std::sync::Arc] if it
-    /// needs to be mutable.
+    /// needs to be mutable, see
+    /// <https://docs.rs/axum/latest/axum/index.html#sharing-state-with-handlers>.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use std::sync::Arc;
     ///
-    /// ### See
+    /// use stackable_webhook::{servers::ConversionWebhookServer, Options};
+    /// use kube::core::conversion::ConversionReview;
     ///
-    /// - <https://docs.rs/axum/latest/axum/index.html#sharing-state-with-handlers>
-    pub fn new_with_state<T, S>(handler: T, state: S, options: Options) -> Self
+    /// #[derive(Debug, Clone)]
+    /// struct State {}
+    ///
+    /// let shared_state = Arc::new(State {});
+    /// let server = ConversionWebhookServer::new_with_state(
+    ///     handler,
+    ///     shared_state,
+    ///     Options::default(),
+    /// );
+    ///
+    /// // Define the handler function
+    /// fn handler(req: ConversionReview, state: Arc<State>) -> ConversionReview {
+    ///    // In here we can do the CRD conversion
+    ///    req
+    /// }
+    /// ```
+    #[instrument(name = "create_conversion_webhook_server_with_state", skip(handler))]
+    pub fn new_with_state<H, S>(handler: H, state: S, options: Options) -> Self
     where
-        T: StatefulWebhookHandler<ConversionReview, ConversionReview, S>
+        H: StatefulWebhookHandler<ConversionReview, ConversionReview, S>
             + Clone
             + Send
             + Sync
             + 'static,
-        S: Clone + Send + Sync + 'static,
+        S: Clone + Debug + Send + Sync + 'static,
     {
-        // See https://github.com/async-graphql/async-graphql/discussions/1150
+        debug!("create new conversion webhook server with state");
+
+        // NOTE (@Techassi): Initially, after adding the state extractor, the
+        // compiler kept throwing a trait error at me stating that the closure
+        // below doesn't implement the Handler trait from Axum. This had nothing
+        // to do with the state itself, but rather the order of extractors. All
+        // body consuming extractors, like the JSON extractor need to come last
+        // in the handler.
+        // https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors
         let handler_fn = |State(state): State<S>, Json(review): Json<ConversionReview>| async {
             let review = handler.call(review, state);
             Json(review)
         };
 
+        debug!("create router");
         let router = Router::new()
             .route("/convert", post(handler_fn))
             .with_state(state);
@@ -82,7 +139,12 @@ impl ConversionWebhookServer {
         Self { router, options }
     }
 
+    /// Starts the conversion webhook server by starting the underlying
+    /// [`WebhookServer`].
+    #[instrument(name = "run_conversion_webhook_server", skip(self), fields(self.options))]
     pub async fn run(self) -> Result<(), crate::Error> {
+        debug!("run conversion webhook server");
+
         let server = WebhookServer::new(self.router, self.options);
         server.run().await
     }

From 94d120b43a655abe7cd2b3edaf8f2534871a1b66 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Mon, 12 Feb 2024 15:49:04 +0100
Subject: [PATCH 18/31] Add doc comments

---
 stackable-webhook/src/constants.rs | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs
index d124133f8..86ec23a6b 100644
--- a/stackable-webhook/src/constants.rs
+++ b/stackable-webhook/src/constants.rs
@@ -2,8 +2,14 @@
 //! addresses.
 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
 
+/// The default HTTPS port `8443`
 pub const DEFAULT_HTTPS_PORT: u16 = 8443;
+
+/// The default HTTP port `8080`.
 pub const DEFAULT_HTTP_PORT: u16 = 8080;
 
+/// The default IP address `127.0.0.1` the webhook server binds to.
 pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
+
+/// The default socket address `127.0.0.1:8443` the webhook server vinds to.
 pub const DEFAULT_SOCKET_ADDR: SocketAddr = SocketAddr::new(DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT);

From f91ead60fc43827ccb9faa228f9190c8d9afd822 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Mon, 12 Feb 2024 15:50:18 +0100
Subject: [PATCH 19/31] Make TLS private key loading configurable

---
 stackable-webhook/src/lib.rs        |  3 ++
 stackable-webhook/src/options.rs    | 14 ++++--
 stackable-webhook/src/tls/certs.rs  | 71 +++++++++++++++--------------
 stackable-webhook/src/tls/server.rs | 10 ++--
 4 files changed, 57 insertions(+), 41 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index a09a02566..9aac51df1 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -165,6 +165,8 @@ impl WebhookServer {
 
 #[cfg(test)]
 mod test {
+    use crate::tls::PrivateKeyEncoding;
+
     use super::*;
     use axum::{routing::get, Router};
 
@@ -175,6 +177,7 @@ mod test {
             .tls_mount(
                 "/tmp/webhook-certs/serverCert.pem",
                 "/tmp/webhook-certs/serverKey.pem",
+                PrivateKeyEncoding::Ec,
             )
             .build();
 
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 0687b05f3..07568a426 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -4,7 +4,10 @@ use std::{
     path::PathBuf,
 };
 
-use crate::constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR};
+use crate::{
+    constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR},
+    tls::PrivateKeyEncoding,
+};
 
 /// Specifies available webhook server options.
 ///
@@ -155,11 +158,13 @@ impl OptionsBuilder {
     pub fn tls_mount(
         mut self,
         cert_path: impl Into<PathBuf>,
-        key_path: impl Into<PathBuf>,
+        pk_path: impl Into<PathBuf>,
+        pk_encoding: PrivateKeyEncoding,
     ) -> Self {
         self.tls = Some(TlsOption::Mount {
             cert_path: cert_path.into(),
-            key_path: key_path.into(),
+            pk_path: pk_path.into(),
+            pk_encoding,
         });
         self
     }
@@ -191,8 +196,9 @@ impl Default for RedirectOption {
 pub enum TlsOption {
     AutoGenerate,
     Mount {
+        pk_encoding: PrivateKeyEncoding,
         cert_path: PathBuf,
-        key_path: PathBuf,
+        pk_path: PathBuf,
     },
 }
 
diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs
index adc082869..88d7b3cd0 100644
--- a/stackable-webhook/src/tls/certs.rs
+++ b/stackable-webhook/src/tls/certs.rs
@@ -1,6 +1,8 @@
+// TODO (@Techassi): Move this into a separate crate which handles TLS cert
+// generation and reading.
 use std::{fs::File, io::BufReader, path::Path};
 
-use rustls_pemfile::{certs, ec_private_keys};
+use rustls_pemfile::{certs, ec_private_keys, pkcs8_private_keys, rsa_private_keys};
 use snafu::{ResultExt, Snafu};
 use tokio_rustls::rustls::{Certificate, PrivateKey};
 
@@ -24,47 +26,43 @@ pub struct CertificateChain {
     private_key: PrivateKey,
 }
 
-impl<C, P> TryFrom<(&mut C, &mut P)> for CertificateChain
-where
-    C: std::io::BufRead,
-    P: std::io::BufRead,
-{
-    type Error = CertifacteError;
+impl CertificateChain {
+    pub fn from_files(
+        cert_path: impl AsRef<Path>,
+        pk_path: impl AsRef<Path>,
+        pk_encoding: PrivateKeyEncoding,
+    ) -> Result<Self, CertifacteError> {
+        let cert_file = File::open(cert_path).context(ReadCertFileSnafu)?;
+        let cert_reader = &mut BufReader::new(cert_file);
+
+        let key_file = File::open(pk_path).context(ReadKeyFileSnafu)?;
+        let key_reader = &mut BufReader::new(key_file);
 
-    fn try_from(readers: (&mut C, &mut P)) -> Result<Self, Self::Error> {
-        let chain = certs(readers.0)
+        Self::from_buffer(cert_reader, key_reader, pk_encoding)
+    }
+
+    fn from_buffer(
+        cert_reader: &mut dyn std::io::BufRead,
+        pk_reader: &mut dyn std::io::BufRead,
+        pk_encoding: PrivateKeyEncoding,
+    ) -> Result<Self, CertifacteError> {
+        let chain = certs(cert_reader)
             .context(ReadBufferedCertFileSnafu)?
             .into_iter()
             .map(Certificate)
             .collect();
 
-        // TODO (@Techassi): Make this function configurable
-        let private_key = ec_private_keys(readers.1)
-            .context(ReadBufferedKeyFileSnafu)?
-            .remove(0);
-        let private_key = PrivateKey(private_key);
+        let pk_bytes = match pk_encoding {
+            PrivateKeyEncoding::Pkcs8 => pkcs8_private_keys(pk_reader),
+            PrivateKeyEncoding::Rsa => rsa_private_keys(pk_reader),
+            PrivateKeyEncoding::Ec => ec_private_keys(pk_reader),
+        }
+        .context(ReadBufferedKeyFileSnafu)?
+        .remove(0);
 
+        let private_key = PrivateKey(pk_bytes);
         Ok(Self { chain, private_key })
     }
-}
-
-impl CertificateChain {
-    pub fn from_files<C, P>(
-        certificate_path: C,
-        private_key_path: P,
-    ) -> Result<Self, CertifacteError>
-    where
-        C: AsRef<Path>,
-        P: AsRef<Path>,
-    {
-        let cert_file = File::open(certificate_path).context(ReadCertFileSnafu)?;
-        let cert_reader = &mut BufReader::new(cert_file);
-
-        let key_file = File::open(private_key_path).context(ReadKeyFileSnafu)?;
-        let key_reader = &mut BufReader::new(key_file);
-
-        Self::try_from((cert_reader, key_reader))
-    }
 
     pub fn chain(&self) -> &[Certificate] {
         &self.chain
@@ -78,3 +76,10 @@ impl CertificateChain {
         (self.chain, self.private_key)
     }
 }
+
+#[derive(Debug)]
+pub enum PrivateKeyEncoding {
+    Pkcs8,
+    Rsa,
+    Ec,
+}
diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs
index a9638a4db..63915aca0 100644
--- a/stackable-webhook/src/tls/server.rs
+++ b/stackable-webhook/src/tls/server.rs
@@ -58,11 +58,13 @@ impl TlsServer {
             }
             TlsOption::Mount {
                 cert_path,
-                key_path,
+                pk_path,
+                pk_encoding,
             } => {
-                let (chain, private_key) = CertificateChain::from_files(cert_path, key_path)
-                    .context(TlsCertificateChainSnafu)?
-                    .into_parts();
+                let (chain, private_key) =
+                    CertificateChain::from_files(cert_path, pk_path, pk_encoding)
+                        .context(TlsCertificateChainSnafu)?
+                        .into_parts();
 
                 // TODO (@Techassi): Use the latest version of rustls related crates
                 let mut config = ServerConfig::builder()

From faae8b2842f4c701e7580bdb0e003b53f67a6519 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 13 Feb 2024 10:45:33 +0100
Subject: [PATCH 20/31] Remove tests

These tests required a lot of manual setup, which
is not desirable. In the future, we need to enable
automatic testing of these servers.
---
 stackable-webhook/src/lib.rs                | 23 ------------
 stackable-webhook/src/servers/conversion.rs | 40 ---------------------
 2 files changed, 63 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 9aac51df1..396b53eaf 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -162,26 +162,3 @@ impl WebhookServer {
         tls_server.run().await.context(RunTlsServerSnafu)
     }
 }
-
-#[cfg(test)]
-mod test {
-    use crate::tls::PrivateKeyEncoding;
-
-    use super::*;
-    use axum::{routing::get, Router};
-
-    #[tokio::test]
-    async fn test() {
-        let router = Router::new().route("/", get(|| async { "Ok" }));
-        let options = Options::builder()
-            .tls_mount(
-                "/tmp/webhook-certs/serverCert.pem",
-                "/tmp/webhook-certs/serverKey.pem",
-                PrivateKeyEncoding::Ec,
-            )
-            .build();
-
-        let server = WebhookServer::new(router, options);
-        server.run().await.unwrap()
-    }
-}
diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs
index efa2f1f64..b1a5690f9 100644
--- a/stackable-webhook/src/servers/conversion.rs
+++ b/stackable-webhook/src/servers/conversion.rs
@@ -149,43 +149,3 @@ impl ConversionWebhookServer {
         server.run().await
     }
 }
-
-#[cfg(test)]
-mod test {
-    use std::sync::Arc;
-
-    use super::*;
-    use crate::Options;
-
-    #[derive(Debug, Clone)]
-    struct State {
-        inner: usize,
-    }
-
-    fn handler(req: ConversionReview) -> ConversionReview {
-        // In here we can do the CRD conversion
-        req
-    }
-
-    fn handler_with_state(req: ConversionReview, state: Arc<State>) -> ConversionReview {
-        println!("{}", state.inner);
-        req
-    }
-
-    #[tokio::test]
-    async fn without_state() {
-        let server = ConversionWebhookServer::new(handler, Options::default());
-        server.run().await.unwrap();
-    }
-
-    #[tokio::test]
-    async fn with_state() {
-        let shared_state = Arc::new(State { inner: 0 });
-        let server = ConversionWebhookServer::new_with_state(
-            handler_with_state,
-            shared_state,
-            Options::default(),
-        );
-        server.run().await.unwrap();
-    }
-}

From 11420a3360271950bc533eba86b200599e622f3e Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 13 Feb 2024 10:47:02 +0100
Subject: [PATCH 21/31] Update rustls-related crates

These new versions introduced quite a few breaking
changes which required a slight reworkf of the TLS
cert handling code.
---
 Cargo.toml                          |  8 ++--
 stackable-webhook/Cargo.toml        |  8 ++--
 stackable-webhook/src/options.rs    |  2 +-
 stackable-webhook/src/tls/certs.rs  | 70 +++++++++++++++++------------
 stackable-webhook/src/tls/mod.rs    |  3 +-
 stackable-webhook/src/tls/server.rs |  3 +-
 6 files changed, 53 insertions(+), 41 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index c491e6a32..a376a682f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,12 +27,12 @@ dockerfile-parser = "0.8.0"
 either = "1.9.0"
 futures = "0.3.28"
 json-patch = "1.0.0"
-k8s-openapi = { version = "0.20.0", default-features = false, features = [
+k8s-openapi = { version = "0.21.0", default-features = false, features = [
     "schemars",
     "v1_28",
 ] }
 # We use rustls instead of openssl for easier portablitly, e.g. so that we can build stackablectl without the need to vendor (build from source) openssl
-kube = { version = "0.87.1", default-features = false, features = [
+kube = { version = "0.88.1", default-features = false, features = [
     "client",
     "jsonpatch",
     "runtime",
@@ -51,9 +51,9 @@ semver = "1.0"
 serde = { version = "1.0.184", features = ["derive"] }
 serde_json = "1.0.104"
 serde_yaml = "0.9.25"
-snafu = "0.7.5"
+snafu = "0.8.0"
 stackable-operator-derive = { path = "stackable-operator-derive" }
-strum = { version = "0.25.0", features = ["derive"] }
+strum = { version = "0.26.1", features = ["derive"] }
 thiserror = "1.0.44"
 time = { version = "0.3.29", optional = true }
 tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml
index 807719341..6a1eea251 100644
--- a/stackable-webhook/Cargo.toml
+++ b/stackable-webhook/Cargo.toml
@@ -8,20 +8,20 @@ repository.workspace = true
 
 [dependencies]
 axum = "0.7.4"
-kube = { version = "0.87.1", default-features = false }
-tokio-rustls = "0.24.1"
+kube = { version = "0.88.1", default-features = false }
+tokio-rustls = "0.25.0"
 serde_json = "1.0.104"
 snafu = "0.8.0"
 tokio = "1.29.1"
 tokio-test = "0.4.3"
 tower = "0.4.13"
 tracing = "0.1.40"
-rustls-pemfile = "1.0.4"
+rustls-pemfile = "2.0.0"
 futures-util = "0.3.30"
 hyper-util = "0.1.3"
 hyper = { version = "1.0.0", features = ["full"] }
 
 [dev-dependencies]
-k8s-openapi = { version = "0.20.0", default-features = false, features = [
+k8s-openapi = { version = "0.21.0", default-features = false, features = [
   "v1_28",
 ] }
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 07568a426..3773b78f8 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -6,7 +6,7 @@ use std::{
 
 use crate::{
     constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR},
-    tls::PrivateKeyEncoding,
+    tls::certs::PrivateKeyEncoding,
 };
 
 /// Specifies available webhook server options.
diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs
index 88d7b3cd0..5f0a5138e 100644
--- a/stackable-webhook/src/tls/certs.rs
+++ b/stackable-webhook/src/tls/certs.rs
@@ -4,7 +4,9 @@ use std::{fs::File, io::BufReader, path::Path};
 
 use rustls_pemfile::{certs, ec_private_keys, pkcs8_private_keys, rsa_private_keys};
 use snafu::{ResultExt, Snafu};
-use tokio_rustls::rustls::{Certificate, PrivateKey};
+use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
+
+pub type Result<T, E = CertifacteError> = std::result::Result<T, E>;
 
 #[derive(Debug, Snafu)]
 pub enum CertifacteError {
@@ -22,8 +24,8 @@ pub enum CertifacteError {
 }
 
 pub struct CertificateChain {
-    chain: Vec<Certificate>,
-    private_key: PrivateKey,
+    chain: Vec<CertificateDer<'static>>,
+    private_key: PrivateKeyDer<'static>,
 }
 
 impl CertificateChain {
@@ -31,48 +33,60 @@ impl CertificateChain {
         cert_path: impl AsRef<Path>,
         pk_path: impl AsRef<Path>,
         pk_encoding: PrivateKeyEncoding,
-    ) -> Result<Self, CertifacteError> {
+    ) -> Result<Self> {
         let cert_file = File::open(cert_path).context(ReadCertFileSnafu)?;
-        let cert_reader = &mut BufReader::new(cert_file);
+        let mut cert_reader = BufReader::new(cert_file);
 
         let key_file = File::open(pk_path).context(ReadKeyFileSnafu)?;
-        let key_reader = &mut BufReader::new(key_file);
+        let mut pk_reader = BufReader::new(key_file);
 
-        Self::from_buffer(cert_reader, key_reader, pk_encoding)
-    }
+        let chain = certs(&mut cert_reader)
+            .collect::<Result<Vec<_>, _>>()
+            .context(ReadBufferedCertFileSnafu)?;
 
-    fn from_buffer(
-        cert_reader: &mut dyn std::io::BufRead,
-        pk_reader: &mut dyn std::io::BufRead,
-        pk_encoding: PrivateKeyEncoding,
-    ) -> Result<Self, CertifacteError> {
-        let chain = certs(cert_reader)
-            .context(ReadBufferedCertFileSnafu)?
-            .into_iter()
-            .map(Certificate)
-            .collect();
-
-        let pk_bytes = match pk_encoding {
-            PrivateKeyEncoding::Pkcs8 => pkcs8_private_keys(pk_reader),
-            PrivateKeyEncoding::Rsa => rsa_private_keys(pk_reader),
-            PrivateKeyEncoding::Ec => ec_private_keys(pk_reader),
+        let private_key = match pk_encoding {
+            PrivateKeyEncoding::Pkcs8 => Self::pkcs8_to_pk_der(&mut pk_reader)?,
+            PrivateKeyEncoding::Rsa => Self::rsa_to_pk_der(&mut pk_reader)?,
+            PrivateKeyEncoding::Ec => Self::ec_to_pk_der(&mut pk_reader)?,
         }
-        .context(ReadBufferedKeyFileSnafu)?
         .remove(0);
 
-        let private_key = PrivateKey(pk_bytes);
         Ok(Self { chain, private_key })
     }
 
-    pub fn chain(&self) -> &[Certificate] {
+    fn pkcs8_to_pk_der<'a>(pk_reader: &mut dyn std::io::BufRead) -> Result<Vec<PrivateKeyDer<'a>>> {
+        let ders = pkcs8_private_keys(pk_reader)
+            .collect::<Result<Vec<_>, _>>()
+            .context(ReadBufferedKeyFileSnafu)?;
+
+        Ok(ders.into_iter().map(PrivateKeyDer::from).collect())
+    }
+
+    fn rsa_to_pk_der<'a>(pk_reader: &mut dyn std::io::BufRead) -> Result<Vec<PrivateKeyDer<'a>>> {
+        let ders = rsa_private_keys(pk_reader)
+            .collect::<Result<Vec<_>, _>>()
+            .context(ReadBufferedKeyFileSnafu)?;
+
+        Ok(ders.into_iter().map(PrivateKeyDer::from).collect())
+    }
+
+    fn ec_to_pk_der<'a>(pk_reader: &mut dyn std::io::BufRead) -> Result<Vec<PrivateKeyDer<'a>>> {
+        let ders = ec_private_keys(pk_reader)
+            .collect::<Result<Vec<_>, _>>()
+            .context(ReadBufferedKeyFileSnafu)?;
+
+        Ok(ders.into_iter().map(PrivateKeyDer::from).collect())
+    }
+
+    pub fn chain(&self) -> &[CertificateDer] {
         &self.chain
     }
 
-    pub fn private_key(&self) -> &PrivateKey {
+    pub fn private_key(&self) -> &PrivateKeyDer {
         &self.private_key
     }
 
-    pub fn into_parts(self) -> (Vec<Certificate>, PrivateKey) {
+    pub fn into_parts(self) -> (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>) {
         (self.chain, self.private_key)
     }
 }
diff --git a/stackable-webhook/src/tls/mod.rs b/stackable-webhook/src/tls/mod.rs
index c180c65ec..7c7918041 100644
--- a/stackable-webhook/src/tls/mod.rs
+++ b/stackable-webhook/src/tls/mod.rs
@@ -1,7 +1,6 @@
 //! Contains structs and functions to easily create a TLS termination server,
 //! which can be used in combination with an Axum [`Router`][axum::Router].
-mod certs;
+pub mod certs;
 mod server;
 
-pub use certs::*;
 pub use server::*;
diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs
index 63915aca0..c06efacf3 100644
--- a/stackable-webhook/src/tls/server.rs
+++ b/stackable-webhook/src/tls/server.rs
@@ -14,7 +14,7 @@ use tracing::{error, instrument, warn};
 
 use crate::{
     options::TlsOption,
-    tls::{CertifacteError, CertificateChain},
+    tls::certs::{CertifacteError, CertificateChain},
 };
 
 pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -68,7 +68,6 @@ impl TlsServer {
 
                 // TODO (@Techassi): Use the latest version of rustls related crates
                 let mut config = ServerConfig::builder()
-                    .with_safe_defaults()
                     .with_no_client_auth()
                     .with_single_cert(chain, private_key)
                     .context(InvalidTlsPrivateKeySnafu)?;

From 795ad648c0ffa1a58ba54b23d494e2577cf91ffa Mon Sep 17 00:00:00 2001
From: Techassi <git@techassi.dev>
Date: Tue, 13 Feb 2024 15:41:41 +0100
Subject: [PATCH 22/31] Apply suggestions

Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com>
---
 stackable-webhook/src/lib.rs     | 2 +-
 stackable-webhook/src/options.rs | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 396b53eaf..93c87444f 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -75,7 +75,7 @@ pub enum Error {
 /// and their handlers are completely customizable by bringing your own
 /// Axum [`Router`].
 ///
-/// For complete complete end-to-end implementations, see
+/// For complete end-to-end implementations, see
 /// [`ConversionWebhookServer`].
 pub struct WebhookServer {
     options: Options,
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 3773b78f8..5f6b3be85 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -157,9 +157,9 @@ impl OptionsBuilder {
     /// [`OptionsBuilder::tls_autogenerate()`] function.
     pub fn tls_mount(
         mut self,
-        cert_path: impl Into<PathBuf>,
-        pk_path: impl Into<PathBuf>,
-        pk_encoding: PrivateKeyEncoding,
+        public_key_path: impl Into<PathBuf>,
+        private_key_path: impl Into<PathBuf>,
+        private_key_encoding: PrivateKeyEncoding,
     ) -> Self {
         self.tls = Some(TlsOption::Mount {
             cert_path: cert_path.into(),

From 2e5341011406264b082c240213c2f49a9224bcea Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 13 Feb 2024 15:49:35 +0100
Subject: [PATCH 23/31] Adjust names according to code style guide

---
 stackable-webhook/src/options.rs    | 12 ++++++------
 stackable-webhook/src/tls/server.rs | 18 ++++++++++--------
 2 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 5f6b3be85..68af8a67a 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -162,9 +162,9 @@ impl OptionsBuilder {
         private_key_encoding: PrivateKeyEncoding,
     ) -> Self {
         self.tls = Some(TlsOption::Mount {
-            cert_path: cert_path.into(),
-            pk_path: pk_path.into(),
-            pk_encoding,
+            public_key_path: public_key_path.into(),
+            private_key_path: private_key_path.into(),
+            private_key_encoding,
         });
         self
     }
@@ -196,9 +196,9 @@ impl Default for RedirectOption {
 pub enum TlsOption {
     AutoGenerate,
     Mount {
-        pk_encoding: PrivateKeyEncoding,
-        cert_path: PathBuf,
-        pk_path: PathBuf,
+        private_key_encoding: PrivateKeyEncoding,
+        public_key_path: PathBuf,
+        private_key_path: PathBuf,
     },
 }
 
diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs
index c06efacf3..16de311fb 100644
--- a/stackable-webhook/src/tls/server.rs
+++ b/stackable-webhook/src/tls/server.rs
@@ -57,16 +57,18 @@ impl TlsServer {
                 todo!()
             }
             TlsOption::Mount {
-                cert_path,
-                pk_path,
-                pk_encoding,
+                public_key_path,
+                private_key_path,
+                private_key_encoding,
             } => {
-                let (chain, private_key) =
-                    CertificateChain::from_files(cert_path, pk_path, pk_encoding)
-                        .context(TlsCertificateChainSnafu)?
-                        .into_parts();
+                let (chain, private_key) = CertificateChain::from_files(
+                    public_key_path,
+                    private_key_path,
+                    private_key_encoding,
+                )
+                .context(TlsCertificateChainSnafu)?
+                .into_parts();
 
-                // TODO (@Techassi): Use the latest version of rustls related crates
                 let mut config = ServerConfig::builder()
                     .with_no_client_auth()
                     .with_single_cert(chain, private_key)

From d0ea673f87dfd903f7f79f78d7f83f6b823bf842 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 13 Feb 2024 15:52:42 +0100
Subject: [PATCH 24/31] Make handler traits only accessible from this crate

---
 stackable-webhook/src/lib.rs | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 93c87444f..d3e661010 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -43,19 +43,20 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
 
 /// A generic webhook handler receiving a request and sending back a response.
 ///
-/// This trait is usually not implemented by external callers and this library
-/// provides various ready-to-use implementations for it. One such an
-pub trait WebhookHandler<Req, Res> {
+/// This trait is not intended to be implemented by external crates and this
+/// library provides various ready-to-use implementations for it. One such an
+/// /// implementation is part of the [`ConversionWebhookServer`].
+pub(crate) trait WebhookHandler<Req, Res> {
     fn call(self, req: Req) -> Res;
 }
 
 /// A generic webhook handler receiving a request and state and sending back
 /// a response.
 ///
-/// This trait is usually not implemented by external callers and this library
-/// provides various ready-to-use implementations for it. One such an
+/// This trait is not intended to be  implemented by external crates and this
+/// library provides various ready-to-use implementations for it. One such an
 /// implementation is part of the [`ConversionWebhookServer`].
-pub trait StatefulWebhookHandler<Req, Res, S> {
+pub(crate) trait StatefulWebhookHandler<Req, Res, S> {
     fn call(self, req: Req, state: S) -> Res;
 }
 

From fa30e3389e79ffbd537581235de0c964627814ee Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 13 Feb 2024 15:58:00 +0100
Subject: [PATCH 25/31] Change option builder function names

---
 stackable-webhook/src/options.rs | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 68af8a67a..84aab0aeb 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -123,24 +123,24 @@ impl OptionsBuilder {
     }
 
     /// Sets the socket address the webhook server uses to bind for HTTPS.
-    pub fn socket_addr(mut self, socket_ip: impl Into<IpAddr>, socket_port: u16) -> Self {
-        self.socket_addr = Some(SocketAddr::new(socket_ip.into(), socket_port));
+    pub fn bind_address(mut self, bind_ip: impl Into<IpAddr>, bind_port: u16) -> Self {
+        self.socket_addr = Some(SocketAddr::new(bind_ip.into(), bind_port));
         self
     }
 
     /// Sets the IP address of the socket address the webhook server uses to
     /// bind for HTTPS.
-    pub fn socket_ip(mut self, socket_ip: impl Into<IpAddr>) -> Self {
+    pub fn bind_ip(mut self, bind_ip: impl Into<IpAddr>) -> Self {
         let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR);
-        addr.set_ip(socket_ip.into());
+        addr.set_ip(bind_ip.into());
         self
     }
 
     /// Sets the port of the socket address the webhook server uses to bind
     /// for HTTPS.
-    pub fn socket_port(mut self, socket_port: u16) -> Self {
+    pub fn bind_port(mut self, bind_port: u16) -> Self {
         let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR);
-        addr.set_port(socket_port);
+        addr.set_port(bind_port);
         self
     }
 

From f7dc0b1efa890c932a6e1f10b4869edd31db661d Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Tue, 13 Feb 2024 16:10:07 +0100
Subject: [PATCH 26/31] Change info! and warn! to debug!

---
 stackable-webhook/src/lib.rs      | 6 +++---
 stackable-webhook/src/redirect.rs | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index d3e661010..26face2f9 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -24,7 +24,7 @@
 //! enable complete controll over these details if needed.
 use axum::Router;
 use snafu::{ResultExt, Snafu};
-use tracing::{debug, info, instrument, warn};
+use tracing::{debug, instrument};
 
 use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer};
 
@@ -141,11 +141,11 @@ impl WebhookServer {
                     http_port,
                 );
 
-                info!(http_port, "spawning redirector in separate task");
+                debug!(http_port, "spawning redirector in separate task");
                 tokio::spawn(redirector.run());
             }
             RedirectOption::Disabled => {
-                warn!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended");
+                debug!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended");
             }
         }
 
diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs
index fb8b0b992..e9ca12d2f 100644
--- a/stackable-webhook/src/redirect.rs
+++ b/stackable-webhook/src/redirect.rs
@@ -68,7 +68,7 @@ impl Redirector {
             // print it in the trace?
             match http_to_https(host, uri.clone(), self.http_port, self.https_port) {
                 Ok(redirect_uri) => {
-                    info!("redirecting from {} to {}", uri, redirect_uri);
+                    debug!("redirecting from {} to {}", uri, redirect_uri);
                     Ok(Redirect::permanent(&redirect_uri.to_string()))
                 }
                 Err(err) => {

From e1f538b71cae0c1d17c72bd903da2d97f280ef51 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Wed, 14 Feb 2024 12:59:25 +0100
Subject: [PATCH 27/31] Add basic high-level request tracing

---
 stackable-webhook/Cargo.toml |  1 +
 stackable-webhook/src/lib.rs | 22 ++++++++++++++++++----
 2 files changed, 19 insertions(+), 4 deletions(-)

diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml
index 6a1eea251..f46c43370 100644
--- a/stackable-webhook/Cargo.toml
+++ b/stackable-webhook/Cargo.toml
@@ -15,6 +15,7 @@ snafu = "0.8.0"
 tokio = "1.29.1"
 tokio-test = "0.4.3"
 tower = "0.4.13"
+tower-http = { version = "0.5.1", features = ["trace"] }
 tracing = "0.1.40"
 rustls-pemfile = "2.0.0"
 futures-util = "0.3.30"
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 26face2f9..e5a983b28 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -22,9 +22,11 @@
 //!
 //! This library additionally also exposes lower-level structs and functions to
 //! enable complete controll over these details if needed.
-use axum::Router;
+use axum::{body::Body, http::Request, Router};
 use snafu::{ResultExt, Snafu};
-use tracing::{debug, instrument};
+use tower::ServiceBuilder;
+use tower_http::trace::TraceLayer;
+use tracing::{debug, debug_span, instrument};
 
 use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer};
 
@@ -149,9 +151,21 @@ impl WebhookServer {
             }
         }
 
+        // TODO (@Techassi): Switch out for Otel compatible tracing
+        // https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk
+
+        // Create a high-level tracing layer
+        debug!("create tracing service (layer)");
+        let layer = TraceLayer::new_for_http()
+            .make_span_with(|_: &Request<Body>| debug_span!("webhook_request"))
+            .on_body_chunk(())
+            .on_eos(());
+
+        let service = ServiceBuilder::new().layer(layer);
+
         // Create the root router and merge the provided router into it.
         debug!("create core couter and merge provided router");
-        let mut router = Router::new();
+        let mut router = Router::new().layer(service);
         router = router.merge(self.router);
 
         // Create server for TLS termination
@@ -159,7 +173,7 @@ impl WebhookServer {
         let tls_server = TlsServer::new(self.options.socket_addr, router, self.options.tls)
             .context(CreateTlsServerSnafu)?;
 
-        info!("running TLS server");
+        debug!("running TLS server");
         tls_server.run().await.context(RunTlsServerSnafu)
     }
 }

From b193cacb7377bbd74b7e6695577e0c7a16aa40e7 Mon Sep 17 00:00:00 2001
From: Techassi <git@techassi.dev>
Date: Wed, 14 Feb 2024 13:02:38 +0100
Subject: [PATCH 28/31] Update stackable-webhook/src/lib.rs

Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com>
---
 stackable-webhook/src/lib.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index e5a983b28..14881a090 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -47,7 +47,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
 ///
 /// This trait is not intended to be implemented by external crates and this
 /// library provides various ready-to-use implementations for it. One such an
-/// /// implementation is part of the [`ConversionWebhookServer`].
+/// implementation is part of the [`ConversionWebhookServer`].
 pub(crate) trait WebhookHandler<Req, Res> {
     fn call(self, req: Req) -> Res;
 }

From dbc9dd3f2a2dc372b2c0d2b96047ff1f35370b42 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Wed, 14 Feb 2024 13:08:24 +0100
Subject: [PATCH 29/31] Remove redirector

---
 stackable-webhook/src/constants.rs |   3 -
 stackable-webhook/src/lib.rs       |  51 +++++++-------
 stackable-webhook/src/options.rs   |  58 +---------------
 stackable-webhook/src/redirect.rs  | 105 -----------------------------
 4 files changed, 28 insertions(+), 189 deletions(-)
 delete mode 100644 stackable-webhook/src/redirect.rs

diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs
index 86ec23a6b..65f7c1ebb 100644
--- a/stackable-webhook/src/constants.rs
+++ b/stackable-webhook/src/constants.rs
@@ -5,9 +5,6 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
 /// The default HTTPS port `8443`
 pub const DEFAULT_HTTPS_PORT: u16 = 8443;
 
-/// The default HTTP port `8080`.
-pub const DEFAULT_HTTP_PORT: u16 = 8080;
-
 /// The default IP address `127.0.0.1` the webhook server binds to.
 pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
 
diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 14881a090..85587989a 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -1,8 +1,7 @@
 //! Utility types and functions to easily create ready-to-use webhook servers
 //! which can handle different tasks, for example CRD conversions. All webhook
-//! servers use HTTPS by default and provides options to enable HTTP to HTTPS
-//! redirection as well. This library is fully compatible with the [`tracing`]
-//! crate and emits multiple levels of tracing data.
+//! servers use HTTPS by defaultThis library is fully compatible with the
+//! [`tracing`] crate and emits debug level tracing data.
 //!
 //! Most users will only use the top-level exported generic [`WebhookServer`]
 //! which enables complete control over the [Router] which handles registering
@@ -28,11 +27,10 @@ use tower::ServiceBuilder;
 use tower_http::trace::TraceLayer;
 use tracing::{debug, debug_span, instrument};
 
-use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer};
+use crate::tls::TlsServer;
 
 pub mod constants;
 pub mod options;
-pub mod redirect;
 pub mod servers;
 pub mod tls;
 
@@ -113,7 +111,6 @@ impl WebhookServer {
     /// use axum::Router;
     ///
     /// let options = Options::builder()
-    ///     .disable_redirect()
     ///     .socket_addr(([127, 0, 0, 1], 8080))
     ///     .build();
     ///
@@ -132,25 +129,6 @@ impl WebhookServer {
     pub async fn run(self) -> Result<()> {
         debug!("run webhook server");
 
-        // Only run the auto redirector when enabled
-        match self.options.redirect {
-            RedirectOption::Enabled(http_port) => {
-                debug!("run webhook server with automatic HTTP to HTTPS redirect enabled");
-
-                let redirector = Redirector::new(
-                    self.options.socket_addr.ip(),
-                    self.options.socket_addr.port(),
-                    http_port,
-                );
-
-                debug!(http_port, "spawning redirector in separate task");
-                tokio::spawn(redirector.run());
-            }
-            RedirectOption::Disabled => {
-                debug!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended");
-            }
-        }
-
         // TODO (@Techassi): Switch out for Otel compatible tracing
         // https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk
 
@@ -177,3 +155,26 @@ impl WebhookServer {
         tls_server.run().await.context(RunTlsServerSnafu)
     }
 }
+
+#[cfg(test)]
+mod test {
+    use crate::tls::certs::PrivateKeyEncoding;
+
+    use super::*;
+    use axum::{routing::get, Router};
+
+    #[tokio::test]
+    async fn test() {
+        let router = Router::new().route("/", get(|| async { "Ok" }));
+        let options = Options::builder()
+            .tls_mount(
+                "/tmp/webhook-certs/serverCert.pem",
+                "/tmp/webhook-certs/serverKey.pem",
+                PrivateKeyEncoding::Pkcs8,
+            )
+            .build();
+
+        let server = WebhookServer::new(router, options);
+        server.run().await.unwrap()
+    }
+}
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 84aab0aeb..93862d603 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -4,17 +4,13 @@ use std::{
     path::PathBuf,
 };
 
-use crate::{
-    constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR},
-    tls::certs::PrivateKeyEncoding,
-};
+use crate::{constants::DEFAULT_SOCKET_ADDR, tls::certs::PrivateKeyEncoding};
 
 /// Specifies available webhook server options.
 ///
 /// The [`Default`] implemention for this struct contains the following
 /// values:
 ///
-/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080
 /// - The socket binds to 127.0.0.1 on port 8443 (HTTPS)
 /// - The TLS cert used gets auto-generated
 ///
@@ -39,22 +35,6 @@ use crate::{
 ///     .build();
 /// ```
 ///
-/// ### Example with Custom Redirects
-///
-/// ```
-/// use stackable_webhook::Options;
-///
-/// // Use a custom HTTP port
-/// let options = Options::builder()
-///     .enable_redirect(12345)
-///     .build();
-///
-/// // Disable auto-redirect
-/// let options = Options::builder()
-///     .disable_redirect()
-///     .build();
-/// ```
-///
 /// ### Example with Mounted TLS Certificate
 ///
 /// ```
@@ -66,14 +46,8 @@ use crate::{
 /// ```
 #[derive(Debug)]
 pub struct Options {
-    /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled,
-    /// it is required to specify the HTTP port. If disabled, the webhook
-    /// server **only** listens on HTTPS.
-    pub redirect: RedirectOption,
-
     /// The default HTTPS socket address the [`TcpListener`][tokio::net::TcpListener]
-    /// binds to. The same IP adress is used for the auto HTTP to HTTPS redirect
-    /// handler.
+    /// binds to.
     pub socket_addr: SocketAddr,
 
     /// Either auto-generate or use an injected TLS certificate.
@@ -102,26 +76,11 @@ impl Options {
 /// [`Options::builder()`] or [`OptionsBuilder::default()`].
 #[derive(Debug, Default)]
 pub struct OptionsBuilder {
-    redirect: Option<RedirectOption>,
     socket_addr: Option<SocketAddr>,
     tls: Option<TlsOption>,
 }
 
 impl OptionsBuilder {
-    /// Disables HTPP to HTTPS auto-redirect entirely. The webhook server
-    /// will only listen on HTTPS.
-    pub fn disable_redirect(mut self) -> Self {
-        self.redirect = Some(RedirectOption::Disabled);
-        self
-    }
-
-    /// Enables HTTP to HTTPS auto-redirect on `http_port`. The webhook
-    /// server will listen on both HTTP and HTTPS.
-    pub fn enable_redirect(mut self, http_port: u16) -> Self {
-        self.redirect = Some(RedirectOption::Enabled(http_port));
-        self
-    }
-
     /// Sets the socket address the webhook server uses to bind for HTTPS.
     pub fn bind_address(mut self, bind_ip: impl Into<IpAddr>, bind_port: u16) -> Self {
         self.socket_addr = Some(SocketAddr::new(bind_ip.into(), bind_port));
@@ -173,25 +132,12 @@ impl OptionsBuilder {
     /// explicitly set option.
     pub fn build(self) -> Options {
         Options {
-            redirect: self.redirect.unwrap_or_default(),
             socket_addr: self.socket_addr.unwrap_or(DEFAULT_SOCKET_ADDR),
             tls: self.tls.unwrap_or_default(),
         }
     }
 }
 
-#[derive(Debug)]
-pub enum RedirectOption {
-    Enabled(u16),
-    Disabled,
-}
-
-impl Default for RedirectOption {
-    fn default() -> Self {
-        Self::Enabled(DEFAULT_HTTP_PORT)
-    }
-}
-
 #[derive(Debug)]
 pub enum TlsOption {
     AutoGenerate,
diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs
deleted file mode 100644
index e9ca12d2f..000000000
--- a/stackable-webhook/src/redirect.rs
+++ /dev/null
@@ -1,105 +0,0 @@
-//! Contains structs and functions to enable auto HTTP to HTTPS redirection.
-use std::net::{IpAddr, SocketAddr};
-
-use axum::{
-    extract::Host,
-    handler::HandlerWithoutStateExt,
-    http::{
-        uri::{InvalidUri, InvalidUriParts, Scheme},
-        StatusCode, Uri,
-    },
-    response::Redirect,
-};
-use snafu::{ResultExt, Snafu};
-use tokio::net::TcpListener;
-use tracing::{debug, info, instrument, warn};
-
-#[derive(Debug, Snafu)]
-pub enum Error {
-    #[snafu(display("failed to parse HTTPS host as authority"))]
-    ParseAuthority { source: InvalidUri },
-
-    #[snafu(display("failed to convert URI parts into URI"))]
-    ConvertPartsToUri { source: InvalidUriParts },
-}
-
-/// A redirector which redirects all incoming HTTP connections to HTTPS
-/// automatically.
-///
-/// Internally it uses a simple handler function which is registered as a
-/// singular [`Service`][tower::MakeService] at the root "/" path. The request
-/// paths are preserved. If the conversion from HTTP to HTTPS fails, the
-/// [`Redirector`] returns a HTTP status code 400 (Bad Request). Additionally,
-/// a warning trace is emitted.
-#[derive(Debug)]
-pub struct Redirector {
-    ip_addr: IpAddr,
-    https_port: u16,
-    http_port: u16,
-}
-
-impl Redirector {
-    #[instrument]
-    pub fn new(ip_addr: IpAddr, https_port: u16, http_port: u16) -> Self {
-        debug!("create new HTTP to HTTPS redirector");
-
-        Self {
-            https_port,
-            http_port,
-            ip_addr,
-        }
-    }
-
-    #[instrument]
-    pub async fn run(self) {
-        debug!("run redirector");
-
-        // The redirector only binds to the HTTP port. The actual HTTPS
-        // application runs in a separate task and is completely independent
-        // of this redirector.
-        let socket_addr = SocketAddr::new(self.ip_addr, self.http_port);
-        let listener = TcpListener::bind(socket_addr).await.unwrap();
-
-        // This converts the HTTP request URI into HTTPS. If this fails, the
-        // redirector emits a warning trace and returns HTTP status code 400
-        // (Bad Request).
-        let redirect = move |Host(host): Host, uri: Uri| async move {
-            // NOTE (@Techassi): Is it worth to clone here just to be able to
-            // print it in the trace?
-            match http_to_https(host, uri.clone(), self.http_port, self.https_port) {
-                Ok(redirect_uri) => {
-                    debug!("redirecting from {} to {}", uri, redirect_uri);
-                    Ok(Redirect::permanent(&redirect_uri.to_string()))
-                }
-                Err(err) => {
-                    warn!(%err, "failed to convert HTTP URI to HTTPS");
-                    Err(StatusCode::BAD_REQUEST)
-                }
-            }
-        };
-
-        // This registers the handler function as the only handler at the root
-        // path "/". See https://docs.rs/axum/latest/axum/fn.serve.html#examples
-        axum::serve(listener, redirect.into_make_service())
-            .await
-            .unwrap();
-    }
-}
-
-fn http_to_https(host: String, uri: Uri, http_port: u16, https_port: u16) -> Result<Uri, Error> {
-    let mut parts = uri.into_parts();
-
-    parts.scheme = Some(Scheme::HTTPS);
-
-    if parts.path_and_query.is_none() {
-        // NOTE (@Techassi): This should never fail and is this save to unwrap.
-        // If this will change into a user-controlled value, then this isn't
-        // save to unwrap anymore and will require explicit error handling.
-        parts.path_and_query = Some("/".parse().unwrap());
-    }
-
-    let https_host = host.replace(&http_port.to_string(), &https_port.to_string());
-    parts.authority = Some(https_host.parse().context(ParseAuthoritySnafu)?);
-
-    Ok(Uri::from_parts(parts).context(ConvertPartsToUriSnafu)?)
-}

From 2ea155295bbd53d1579fd125f0c079e5bf2645f6 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Wed, 14 Feb 2024 13:13:03 +0100
Subject: [PATCH 30/31] Fix doc tests

---
 stackable-webhook/src/lib.rs     |  2 +-
 stackable-webhook/src/options.rs | 14 +++++++++-----
 2 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 85587989a..10e65d594 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -111,7 +111,7 @@ impl WebhookServer {
     /// use axum::Router;
     ///
     /// let options = Options::builder()
-    ///     .socket_addr(([127, 0, 0, 1], 8080))
+    ///     .bind_address([127, 0, 0, 1], 8080)
     ///     .build();
     ///
     /// let router = Router::new();
diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs
index 93862d603..49349da17 100644
--- a/stackable-webhook/src/options.rs
+++ b/stackable-webhook/src/options.rs
@@ -21,27 +21,31 @@ use crate::{constants::DEFAULT_SOCKET_ADDR, tls::certs::PrivateKeyEncoding};
 ///
 /// // Set IP address and port at the same time
 /// let options = Options::builder()
-///     .socket_addr([0, 0, 0, 0], 12345)
+///     .bind_address([0, 0, 0, 0], 12345)
 ///     .build();
 ///
 /// // Set IP address only
 /// let options = Options::builder()
-///     .socket_ip([0, 0, 0, 0])
+///     .bind_ip([0, 0, 0, 0])
 ///     .build();
 ///
 /// // Set port only
 /// let options = Options::builder()
-///     .socket_port(12345)
+///     .bind_port(12345)
 ///     .build();
 /// ```
 ///
 /// ### Example with Mounted TLS Certificate
 ///
 /// ```
-/// use stackable_webhook::Options;
+/// use stackable_webhook::{Options, tls::certs::PrivateKeyEncoding};
 ///
 /// let options = Options::builder()
-///     .tls_mount("path/to/pem/cert", "path/to/pem/key")
+///     .tls_mount(
+///         "path/to/pem/cert",
+///         "path/to/pem/key",
+///         PrivateKeyEncoding::Pkcs8,
+///     )
 ///     .build();
 /// ```
 #[derive(Debug)]

From e9ffe0e40b49ffde97ffd60883ee741e294a28b8 Mon Sep 17 00:00:00 2001
From: Techassi <sascha.lautenschlaeger@stackable.tech>
Date: Wed, 14 Feb 2024 13:15:09 +0100
Subject: [PATCH 31/31] Fix typo

---
 stackable-webhook/src/lib.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs
index 10e65d594..0518adaaf 100644
--- a/stackable-webhook/src/lib.rs
+++ b/stackable-webhook/src/lib.rs
@@ -142,7 +142,7 @@ impl WebhookServer {
         let service = ServiceBuilder::new().layer(layer);
 
         // Create the root router and merge the provided router into it.
-        debug!("create core couter and merge provided router");
+        debug!("create core router and merge provided router");
         let mut router = Router::new().layer(service);
         router = router.merge(self.router);