From 525a77f6e530551eb9ad96e8c19e1bc1483451bb Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Tue, 2 May 2023 15:31:24 +0100 Subject: [PATCH 1/4] Refactor CLI arguments into separate module --- src/cli.rs | 34 ++++++++++++++++++++++++++++++++++ src/main.rs | 34 ++-------------------------------- 2 files changed, 36 insertions(+), 32 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..1df31a0 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,34 @@ +//! Command Line Interface (CLI) arguments. + +use clap::Parser; + +/// S3 Active Storage Proxy command line interface +#[derive(Debug, Parser)] +pub struct CommandLineArgs { + /// The IP address on which the proxy should listen + #[arg(long, default_value = "0.0.0.0", env = "S3_ACTIVE_STORAGE_HOST")] + pub host: String, + /// The port to which the proxy should bind + #[arg(long, default_value_t = 8080, env = "S3_ACTIVE_STORAGE_PORT")] + pub port: u16, + /// Flag indicating whether HTTPS should be used + #[arg(long, default_value_t = false, env = "S3_ACTIVE_STORAGE_HTTPS")] + pub https: bool, + /// Path to the certificate file to be used for HTTPS encryption + #[arg( + long, + default_value = "~/.config/s3-active-storage/certs/cert.pem", + env = "S3_ACTIVE_STORAGE_CERT_FILE" + )] + pub cert_file: String, + /// Path to the key file to be used for HTTPS encryption + #[arg( + long, + default_value = "~/.config/s3-active-storage/certs/key.pem", + env = "S3_ACTIVE_STORAGE_KEY_FILE" + )] + pub key_file: String, + /// Maximum time in seconds to wait for operations to complete upon receiving `ctrl+c` signal. + #[arg(long, default_value_t = 60, env = "S3_ACTIVE_STORAGE_SHUTDOWN_TIMEOUT")] + pub graceful_shutdown_timeout: u64, +} diff --git a/src/main.rs b/src/main.rs index 26bc0bc..2f2fdf4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod app; mod array; +mod cli; mod error; mod models; mod operation; @@ -40,41 +41,10 @@ mod operations; mod s3_client; mod validated_json; -/// S3 Active Storage Proxy command line interface -#[derive(Debug, Parser)] -struct CommandLineArgs { - /// The IP address on which the proxy should listen - #[arg(long, default_value = "0.0.0.0", env = "S3_ACTIVE_STORAGE_HOST")] - host: String, - /// The port to which the proxy should bind - #[arg(long, default_value_t = 8080, env = "S3_ACTIVE_STORAGE_PORT")] - port: u16, - /// Flag indicating whether HTTPS should be used - #[arg(long, default_value_t = false, env = "S3_ACTIVE_STORAGE_HTTPS")] - https: bool, - /// Path to the certificate file to be used for HTTPS encryption - #[arg( - long, - default_value = "~/.config/s3-active-storage/certs/cert.pem", - env = "S3_ACTIVE_STORAGE_CERT_FILE" - )] - cert_file: String, - /// Path to the key file to be used for HTTPS encryption - #[arg( - long, - default_value = "~/.config/s3-active-storage/certs/key.pem", - env = "S3_ACTIVE_STORAGE_KEY_FILE" - )] - key_file: String, - /// Maximum time in seconds to wait for operations to complete upon receiving `ctrl+c` signal. - #[arg(long, default_value_t = 60, env = "S3_ACTIVE_STORAGE_SHUTDOWN_TIMEOUT")] - graceful_shutdown_timeout: u64, -} - /// Application entry point #[tokio::main] async fn main() { - let args = CommandLineArgs::parse(); + let args = cli::CommandLineArgs::parse(); init_tracing(); From aabb5e06d2eb513f33dcf57ca4ac342b061419c0 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Tue, 2 May 2023 15:48:43 +0100 Subject: [PATCH 2/4] Refactor server into server.rs --- src/app.rs | 14 ++++--- src/main.rs | 92 +------------------------------------------- src/server.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 95 deletions(-) create mode 100644 src/server.rs diff --git a/src/app.rs b/src/app.rs index 9f85492..e4f9d14 100644 --- a/src/app.rs +++ b/src/app.rs @@ -86,7 +86,14 @@ fn router() -> Router { .nest("/v1", v1()) } -/// Returns a [tower_service::Service] for the Active Storage server API +/// S3 Active Storage Server Service type alias +/// +/// This type implements [tower_service::Service]. +// FIXME: The Service type should be some form of tower_service::Service, but couldn't find the +// necessary trait bounds. +pub type Service = tower_http::normalize_path::NormalizePath; + +/// Returns a [crate::app::Service] for the Active Storage server API /// /// The service is populated with all routes as well as the following middleware: /// @@ -95,10 +102,7 @@ fn router() -> Router { /// headers /// * a [tower_http::normalize_path::NormalizePathLayer] for trimming trailing slashes from /// requests -pub fn service() -> tower_http::normalize_path::NormalizePath { - // FIXME: The return type should be some form of tower_service::Service, but couldn't find the - // necessary trait bounds. - +pub fn service() -> Service { // Note that any middleware that should affect routing must wrap the router. // See // https://docs.rs/axum/0.6.18/axum/middleware/index.html#rewriting-request-uri-in-middleware. diff --git a/src/main.rs b/src/main.rs index 2f2fdf4..aa8feac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,13 +22,7 @@ //! * [ndarray] provides [NumPy](https://numpy.orgq)-like n-dimensional arrays used in numerical //! computation. -use std::{net::SocketAddr, process::exit, str::FromStr, time::Duration}; - -use axum::ServiceExt; -use axum_server::{tls_rustls::RustlsConfig, Handle}; use clap::Parser; -use expanduser::expanduser; -use tokio::signal; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod app; @@ -39,6 +33,7 @@ mod models; mod operation; mod operations; mod s3_client; +mod server; mod validated_json; /// Application entry point @@ -49,59 +44,7 @@ async fn main() { init_tracing(); let service = app::service(); - let addr = SocketAddr::from_str(&format!("{}:{}", args.host, args.port)) - .expect("invalid host name, IP address or port number"); - - // Catch ctrl+c and try to shutdown gracefully - let handle = Handle::new(); - tokio::spawn(shutdown_signal( - handle.clone(), - args.graceful_shutdown_timeout, - )); - - if args.https { - // Expand files - let abs_cert_file = expanduser(args.cert_file) - .expect("Failed to expand ~ to user name. Please provide an absolute path instead.") - .canonicalize() - .expect("failed to determine absolute path to TLS cerficate file"); - let abs_key_file = expanduser(args.key_file) - .expect("Failed to expand ~ to user name. Please provide an absolute path instead.") - .canonicalize() - .expect("failed to determine absolute path to TLS key file"); - // Check files exist - if !abs_cert_file.exists() { - println!( - "TLS certificate file expected at '{}' but not found.", - abs_cert_file.display() - ); - exit(1) - } - if !abs_key_file.exists() { - println!( - "TLS key file expected at '{}' but not found.", - abs_key_file.display() - ); - exit(1) - } - // Set up TLS config - let tls_config = RustlsConfig::from_pem_file(abs_cert_file, abs_key_file) - .await - .expect("Failed to load TLS certificate files"); - // run HTTPS server with hyper - axum_server::bind_rustls(addr, tls_config) - .handle(handle) - .serve(service.into_make_service()) - .await - .unwrap(); - } else { - // run HTTP server with hyper - axum_server::bind(addr) - .handle(handle) - .serve(service.into_make_service()) - .await - .unwrap(); - } + server::serve(&args, service).await; } /// Initlialise tracing (logging) @@ -117,34 +60,3 @@ fn init_tracing() { .with(tracing_subscriber::fmt::layer()) .init(); } - -/// Graceful shutdown handler -/// -/// Installs signal handlers to catch Ctrl-C or SIGTERM and trigger a graceful shutdown. -async fn shutdown_signal(handle: Handle, timeout: u64) { - let ctrl_c = async { - signal::ctrl_c() - .await - .expect("failed to install Ctrl+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, - } - - println!("signal received, starting graceful shutdown"); - // Force shutdown if graceful shutdown takes longer than 10s - handle.graceful_shutdown(Some(Duration::from_secs(timeout))); -} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..f52a951 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,103 @@ +//! Web server + +use crate::cli; + +use std::{net::SocketAddr, process::exit, str::FromStr, time::Duration}; + +use axum::ServiceExt; +use axum_server::{tls_rustls::RustlsConfig, Handle}; +use expanduser::expanduser; +use tokio::signal; + +/// Serve the S3 Active Storage service +/// +/// # Arguments +/// +/// * `args`: Command line arguments +/// * `service`: The [crate::app::Service] to serve +pub async fn serve(args: &cli::CommandLineArgs, service: crate::app::Service) { + let addr = SocketAddr::from_str(&format!("{}:{}", args.host, args.port)) + .expect("invalid host name, IP address or port number"); + + // Catch ctrl+c and try to shutdown gracefully + let handle = Handle::new(); + tokio::spawn(shutdown_signal( + handle.clone(), + args.graceful_shutdown_timeout, + )); + + if args.https { + // Expand files + let abs_cert_file = expanduser(&args.cert_file) + .expect("Failed to expand ~ to user name. Please provide an absolute path instead.") + .canonicalize() + .expect("failed to determine absolute path to TLS cerficate file"); + let abs_key_file = expanduser(&args.key_file) + .expect("Failed to expand ~ to user name. Please provide an absolute path instead.") + .canonicalize() + .expect("failed to determine absolute path to TLS key file"); + // Check files exist + if !abs_cert_file.exists() { + println!( + "TLS certificate file expected at '{}' but not found.", + abs_cert_file.display() + ); + exit(1) + } + if !abs_key_file.exists() { + println!( + "TLS key file expected at '{}' but not found.", + abs_key_file.display() + ); + exit(1) + } + // Set up TLS config + let tls_config = RustlsConfig::from_pem_file(abs_cert_file, abs_key_file) + .await + .expect("Failed to load TLS certificate files"); + // run HTTPS server with hyper + axum_server::bind_rustls(addr, tls_config) + .handle(handle) + .serve(service.into_make_service()) + .await + .unwrap(); + } else { + // run HTTP server with hyper + axum_server::bind(addr) + .handle(handle) + .serve(service.into_make_service()) + .await + .unwrap(); + } +} + +/// Graceful shutdown handler +/// +/// Installs signal handlers to catch Ctrl-C or SIGTERM and trigger a graceful shutdown. +async fn shutdown_signal(handle: Handle, timeout: u64) { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + println!("signal received, starting graceful shutdown"); + // Force shutdown if graceful shutdown takes longer than 10s + handle.graceful_shutdown(Some(Duration::from_secs(timeout))); +} From a679ad2caa87b4b67f22f31db2b45c4814e747c5 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Tue, 2 May 2023 15:51:21 +0100 Subject: [PATCH 3/4] Refactor tracing into tracing.rs --- src/main.rs | 20 ++------------------ src/tracing.rs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 src/tracing.rs diff --git a/src/main.rs b/src/main.rs index aa8feac..e051d0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,6 @@ //! computation. use clap::Parser; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod app; mod array; @@ -34,29 +33,14 @@ mod operation; mod operations; mod s3_client; mod server; +mod tracing; mod validated_json; /// Application entry point #[tokio::main] async fn main() { let args = cli::CommandLineArgs::parse(); - - init_tracing(); - + tracing::init_tracing(); let service = app::service(); server::serve(&args, service).await; } - -/// Initlialise tracing (logging) -/// -/// Applies a filter based on the `RUST_LOG` environment variable, falling back to enable debug -/// logging for this crate and tower_http if not set. -fn init_tracing() { - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "s3_active_storage=debug,tower_http=debug".into()), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); -} diff --git a/src/tracing.rs b/src/tracing.rs new file mode 100644 index 0000000..53375b6 --- /dev/null +++ b/src/tracing.rs @@ -0,0 +1,17 @@ +//! Tracing (logging) + +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +/// Initlialise tracing (logging) +/// +/// Applies a filter based on the `RUST_LOG` environment variable, falling back to enable debug +/// logging for this crate and tower_http if not set. +pub fn init_tracing() { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "s3_active_storage=debug,tower_http=debug".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); +} From fd17afcbb89739a788f4fe13fb66b754b5a9c568 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Tue, 2 May 2023 15:54:43 +0100 Subject: [PATCH 4/4] cli: add parse function Avoids the need to import the Parser trait. --- src/cli.rs | 5 +++++ src/main.rs | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1df31a0..5769690 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,3 +32,8 @@ pub struct CommandLineArgs { #[arg(long, default_value_t = 60, env = "S3_ACTIVE_STORAGE_SHUTDOWN_TIMEOUT")] pub graceful_shutdown_timeout: u64, } + +/// Returns parsed command line arguments. +pub fn parse() -> CommandLineArgs { + CommandLineArgs::parse() +} diff --git a/src/main.rs b/src/main.rs index e051d0d..df99b93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,8 +22,6 @@ //! * [ndarray] provides [NumPy](https://numpy.orgq)-like n-dimensional arrays used in numerical //! computation. -use clap::Parser; - mod app; mod array; mod cli; @@ -39,7 +37,7 @@ mod validated_json; /// Application entry point #[tokio::main] async fn main() { - let args = cli::CommandLineArgs::parse(); + let args = cli::parse(); tracing::init_tracing(); let service = app::service(); server::serve(&args, service).await;