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