initial commit; basic server skeleton w/ health endpoint
Through section 3.5.
This commit is contained in:
commit
6a9f4c87c0
10 changed files with 1710 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1490
Cargo.lock
generated
Normal file
1490
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
Cargo.toml
Normal file
26
Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "zero-to-axum"
|
||||
version = "0.1.0-dev"
|
||||
authors = [ "azdle <azdle@azdle.net>" ]
|
||||
edition = "2021"
|
||||
description = "An axum based HTTP server template."
|
||||
repository = "https://git.idlestate.org/azdle/zero-to-axum"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.71", features = ["backtrace"] }
|
||||
axum = "0.6.18"
|
||||
futures-util = "0.3"
|
||||
hyper = "0.14.26"
|
||||
pin-project = "1.1.0"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
serde_json = "1.0.99"
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3", features =["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = "0.11.18"
|
||||
test-log = { version = "0.2.12", default-features = false, features = ["trace"] }
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
FROM rust:alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN apk add musl-dev
|
||||
RUN cargo build --release
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
LABEL org.opencontainers.image.source https://git.idlestate.org/azdle/zero-to-axum
|
||||
|
||||
COPY --from=builder /app/target/release/zero-to-axum /usr/local/bin/zero-to-axum
|
||||
|
||||
CMD ["zero-to-axum"]
|
10
README.md
Normal file
10
README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Zero to Axum
|
||||
|
||||
A template Axum server made from (loosely) following the book "Zero to Production in Rust".
|
||||
Instead of making a mailing list manager, it manages local user accounts.
|
||||
|
||||
## Endpoints
|
||||
|
||||
###`/health`
|
||||
|
||||
Responds with an empty 200 respose if the server is healthy.
|
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod server;
|
||||
|
||||
pub use server::ZeroToAxum;
|
32
src/main.rs
Normal file
32
src/main.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use anyhow::{Context, Result};
|
||||
use tokio::signal;
|
||||
use tracing::info;
|
||||
use zero_to_axum::ZeroToAxum;
|
||||
|
||||
fn init_tracing() {
|
||||
use tracing_subscriber::{filter::LevelFilter, fmt, EnvFilter};
|
||||
|
||||
let log_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
fmt().pretty().with_env_filter(log_filter).init();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
init_tracing();
|
||||
|
||||
let addr = "[::]:3742".parse().unwrap();
|
||||
let mut server = ZeroToAxum::serve(addr);
|
||||
|
||||
// Turn a SIGINT into a graceful shutdown
|
||||
let server_shutdown = server.take_shutdown_handle().unwrap();
|
||||
tokio::spawn(async {
|
||||
signal::ctrl_c().await.unwrap();
|
||||
info!("[Ctrl-C] Shutting Down");
|
||||
server_shutdown.send(()).unwrap();
|
||||
});
|
||||
|
||||
server.await.context("run server")
|
||||
}
|
70
src/server/mod.rs
Normal file
70
src/server/mod.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
pub mod routes;
|
||||
|
||||
use anyhow::Result;
|
||||
use pin_project::pin_project;
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::pin;
|
||||
use std::pin::Pin;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::info;
|
||||
|
||||
#[pin_project]
|
||||
pub struct ZeroToAxum {
|
||||
#[pin]
|
||||
server: Pin<Box<dyn Future<Output = Result<(), hyper::Error>> + Send>>,
|
||||
bound_addr: SocketAddr,
|
||||
shutdown: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl Future for ZeroToAxum {
|
||||
type Output = Result<(), hyper::Error>;
|
||||
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
self.project().server.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ZeroToAxum {
|
||||
pub fn shutdown(self) -> Result<()> {
|
||||
if let Some(shutdown) = self.shutdown {
|
||||
shutdown
|
||||
.send(())
|
||||
.map_err(|()| anyhow::anyhow!("failed to send"))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("shutdown handle gone"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_shutdown_handle(&mut self) -> Option<oneshot::Sender<()>> {
|
||||
self.shutdown.take()
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.bound_addr
|
||||
}
|
||||
|
||||
pub fn serve(addr: SocketAddr) -> ZeroToAxum {
|
||||
let app = routes::build();
|
||||
|
||||
let (shutdown_sender, shutdown_receiver) = oneshot::channel();
|
||||
|
||||
let server = axum::Server::bind(&addr).serve(app.into_make_service());
|
||||
|
||||
let bound_addr = server.local_addr();
|
||||
info!("server started, listening on {addr:?}");
|
||||
|
||||
let server = server.with_graceful_shutdown(async {
|
||||
shutdown_receiver.await.ok();
|
||||
});
|
||||
|
||||
ZeroToAxum {
|
||||
server: Box::pin(server),
|
||||
bound_addr,
|
||||
shutdown: Some(shutdown_sender),
|
||||
}
|
||||
}
|
||||
}
|
8
src/server/routes/mod.rs
Normal file
8
src/server/routes/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use axum::{routing::get, Router};
|
||||
|
||||
pub fn build() -> Router {
|
||||
Router::new().route("/health", get(health_check))
|
||||
}
|
||||
|
||||
// just always returns a 200 OK for now, the server has no state, if it's up, it's working
|
||||
pub async fn health_check() {}
|
53
tests/basic.rs
Normal file
53
tests/basic.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use anyhow::{Context, Result};
|
||||
use futures_util::FutureExt;
|
||||
use std::net::SocketAddr;
|
||||
use test_log::test;
|
||||
use tokio::{sync::oneshot, task::JoinHandle};
|
||||
use tracing::info;
|
||||
use zero_to_axum::ZeroToAxum;
|
||||
|
||||
struct TestServer {
|
||||
server_task_handle: JoinHandle<()>,
|
||||
shutdown_handle: oneshot::Sender<()>,
|
||||
addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
async fn spawn() -> TestServer {
|
||||
info!("start server");
|
||||
let mut server = ZeroToAxum::serve("[::]:0".parse().unwrap());
|
||||
let shutdown_handle = server.take_shutdown_handle().unwrap();
|
||||
let addr = server.local_addr();
|
||||
let server_task_handle = tokio::spawn(server.map(|res| res.unwrap()));
|
||||
info!("server spawned");
|
||||
|
||||
TestServer {
|
||||
server_task_handle,
|
||||
shutdown_handle,
|
||||
addr,
|
||||
}
|
||||
}
|
||||
|
||||
/// format a URL for the given path
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!("http://{}{path}", self.addr)
|
||||
}
|
||||
|
||||
/// Request a graceful shutdown and then wait for shutdown to complete
|
||||
async fn shutdown(self) -> Result<()> {
|
||||
self.shutdown_handle.send(()).ok();
|
||||
self.server_task_handle
|
||||
.await
|
||||
.context("wait for server shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn health_check() -> Result<()> {
|
||||
let server = TestServer::spawn().await;
|
||||
let status = reqwest::get(server.url("/health")).await?.status();
|
||||
|
||||
assert_eq!(status, 200, "health check failed");
|
||||
|
||||
server.shutdown().await
|
||||
}
|
Loading…
Add table
Reference in a new issue