feat(api): axum skeleton with health and OpenAPI (#53)
ci / web (push) Successful in 6s
ci / rust (push) Successful in 45s
e2e / e2e (push) Successful in 1m31s

This commit was merged in pull request #53.
This commit is contained in:
2026-08-22 20:09:52 +01:00
parent 03ca4a26e8
commit 58827da647
10 changed files with 910 additions and 16 deletions
+105 -7
View File
@@ -4,15 +4,113 @@ mod config;
use std::process::ExitCode;
fn main() -> ExitCode {
match config::Config::load() {
Ok(config) => {
println!("arr starting, bind_addr={}", config.bind_addr);
ExitCode::SUCCESS
}
use arr_api::{AppState, Upstreams};
use config::Config;
use tower_http::trace::TraceLayer;
/// Dump the `OpenAPI` document and exit, instead of serving. `just gen-client`
/// uses this so the TypeScript client can be regenerated without a port or a
/// single upstream being up.
const OPENAPI_FLAG: &str = "--openapi";
#[tokio::main]
async fn main() -> ExitCode {
if std::env::args().nth(1).as_deref() == Some(OPENAPI_FLAG) {
return dump_openapi();
}
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,tower_http=debug".into()),
)
.init();
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("arr: {err}");
tracing::error!("{err}");
ExitCode::FAILURE
}
}
}
fn dump_openapi() -> ExitCode {
match arr_api::openapi().to_pretty_json() {
Ok(json) => {
println!("{json}");
ExitCode::SUCCESS
}
Err(err) => {
eprintln!("arr: openapi: {err}");
ExitCode::FAILURE
}
}
}
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("config: {0}")]
Config(#[from] config::ConfigError),
#[error("http client: {0}")]
HttpClient(#[from] reqwest::Error),
#[error("bind {addr}: {source}")]
Bind {
addr: std::net::SocketAddr,
source: std::io::Error,
},
#[error("serve: {0}")]
Serve(std::io::Error),
}
async fn run() -> Result<(), Error> {
let config = Config::load()?;
let state = AppState::new(
Upstreams::new(config.prowlarr_url, config.transmission_url)
.with_prowlarr_api_key(config.prowlarr_api_key)
.with_tmdb_api_key(config.tmdb_api_key),
)?;
let app = arr_api::router(state).layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind(config.bind_addr)
.await
.map_err(|source| Error::Bind {
addr: config.bind_addr,
source,
})?;
tracing::info!(addr = %config.bind_addr, docs = arr_api::DOCS_PATH, "listening");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown())
.await
.map_err(Error::Serve)
}
/// Stop accepting on Ctrl-C, or on the SIGTERM a service manager sends.
async fn shutdown() {
let interrupt = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let terminate = async {
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
Ok(mut signal) => {
signal.recv().await;
}
Err(err) => tracing::warn!("no SIGTERM handler: {err}"),
}
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = interrupt => {},
() = terminate => {},
}
tracing::info!("shutting down");
}