Skip to content

feat: Add support for webhook servers #730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a1de224
Initial commit
Techassi Jan 26, 2024
67d1480
Merge branch 'main' into feat/webhooks
Techassi Jan 26, 2024
13612c1
Start redirector impl
Techassi Feb 5, 2024
66a54ae
Improve redirector impl
Techassi Feb 5, 2024
5dc336e
Add doc comments, add traces
Techassi Feb 6, 2024
74b874a
Add options builder, move into own module
Techassi Feb 6, 2024
bda6e59
Add initial TLS acceptor
Techassi Feb 6, 2024
f1af892
Remove unused module
Techassi Feb 6, 2024
67194d1
Change hard-coded cert paths
Techassi Feb 6, 2024
2c3739f
Start TLS server cleanup
Techassi Feb 6, 2024
d00d38c
Move code, abstract away certificate chain
Techassi Feb 6, 2024
96037fc
Change private key load function
Techassi Feb 6, 2024
c62d65c
Remove private key test
Techassi Feb 6, 2024
7bc7329
Add many doc comments, start bubbling up errors
Techassi Feb 8, 2024
05c2d65
Add doc comments, continue error handling
Techassi Feb 9, 2024
00f5a8b
Add ready-to-use conversion webhook server
Techassi Feb 9, 2024
69714e8
Add support for state in ready-to-use conversion webhook server
Techassi Feb 12, 2024
d4db28d
Adjust doc comments
Techassi Feb 12, 2024
94d120b
Add doc comments
Techassi Feb 12, 2024
f91ead6
Make TLS private key loading configurable
Techassi Feb 12, 2024
367f8b4
Merge branch 'main' into feat/webhooks
Techassi Feb 12, 2024
9d9f1a9
Merge branch 'main' into feat/webhooks
Techassi Feb 12, 2024
faae8b2
Remove tests
Techassi Feb 13, 2024
11420a3
Update rustls-related crates
Techassi Feb 13, 2024
795ad64
Apply suggestions
Techassi Feb 13, 2024
2e53410
Adjust names according to code style guide
Techassi Feb 13, 2024
d0ea673
Make handler traits only accessible from this crate
Techassi Feb 13, 2024
fa30e33
Change option builder function names
Techassi Feb 13, 2024
f7dc0b1
Change info! and warn! to debug!
Techassi Feb 13, 2024
e1f538b
Add basic high-level request tracing
Techassi Feb 14, 2024
b193cac
Update stackable-webhook/src/lib.rs
Techassi Feb 14, 2024
dbc9dd3
Remove redirector
Techassi Feb 14, 2024
2ea1552
Fix doc tests
Techassi Feb 14, 2024
e9ffe0e
Fix typo
Techassi Feb 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"] }
Expand All @@ -67,4 +67,4 @@ rstest = "0.18.1"
tempfile = "3.7.1"

[workspace]
members = ["stackable-operator-derive"]
members = ["stackable-operator-derive", "stackable-webhook"]
27 changes: 27 additions & 0 deletions stackable-webhook/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[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.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 = "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.21.0", default-features = false, features = [
"v1_28",
] }
15 changes: 15 additions & 0 deletions stackable-webhook/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Contains various constant definitions, mostly for default ports and IP
//! 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);
165 changes: 165 additions & 0 deletions stackable-webhook/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//! 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.
//!
//! 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 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`].
//!
//! 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, instrument};

use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer};

pub mod constants;
pub mod options;
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 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 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 StatefulWebhookHandler<Req, Res, S> {
fn call(self, req: Req, state: S) -> Res;
}

#[derive(Debug, Snafu)]
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.
///
/// 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`].
///
/// For complete end-to-end implementations, see
/// [`ConversionWebhookServer`].
pub struct WebhookServer {
options: Options,
router: Router,
}

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.
///
/// 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};
/// use axum::Router;
///
/// let router = Router::new();
/// let server = WebhookServer::new(router, Options::default());
/// ```
///
/// ### 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);
/// ```
#[instrument(name = "create_webhook_server", skip(router))]
pub 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.
#[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
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");
}
}

// 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
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)
}
}
Loading