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