Implement API setup with configuration management and startup tasks
- Add `Cargo.toml` for API with dependencies. - Create `config.rs` for managing application settings. - Implement logging and server settings in `config.rs`. - Add `main.rs` to initialize the application and handle database connections. - Introduce `task` module with startup tasks, including database migrations. - Update `.gitignore` to exclude `config.yaml` and remove `.gitkeep`.
This commit is contained in:
1
apps/api/.gitignore
vendored
Normal file
1
apps/api/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
config.yaml
|
||||
19
apps/api/Cargo.toml
Normal file
19
apps/api/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "yet-another-nginx-proxy-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
database = { path = "../../public/database" }
|
||||
migration = { path = "../../public/migration" }
|
||||
|
||||
axum = { version = "0.8.7", features = ["form", "http1", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing", "macros"]}
|
||||
chrono = { version = "0.4.42", features = ["clock", "std", "oldtime", "wasmbind", "serde"] }
|
||||
config = { version = "0.15.19", features = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"] }
|
||||
tokio = { version = "1", features = ["fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "sync", "time", "tracing"] }
|
||||
tower = { version = "0.5.2", features = ["tokio", "tracing"] }
|
||||
tower-http = { version = "0.6.6" }
|
||||
tracing = { version = "0.1.41", features = ["std", "attributes"] }
|
||||
tracing-subscriber = { version = "0.3.20", features = ["smallvec", "fmt", "ansi", "tracing-log", "std", "chrono", "json", "serde", "serde_json", "time", "tracing"] }
|
||||
serde_json = { version = "1.0.145", features = ["std"] }
|
||||
serde = { version = "1.0.228", features = ["std", "derive"] }
|
||||
218
apps/api/src/config.rs
Normal file
218
apps/api/src/config.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use config::{Config, ConfigError};
|
||||
use tracing::{Level, debug, error, warn};
|
||||
|
||||
const LOGGING_LEVEL_KEY: &str = "LOGGING.LEVEL";
|
||||
const LOGGING_UTC_KEY: &str = "LOGGING.UTC";
|
||||
const SERVER_ADDRESS_KEY: &str = "SERVER.ADDRESS";
|
||||
const SERVER_PORT_KEY: &str = "SERVER.PORT";
|
||||
const DATABASE_URL_KEY: &str = "DATABASE.URL";
|
||||
const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS";
|
||||
const DATABASE_MIGRATE_ON_STARTUP_KEY: &str = "DATABASE.MIGRATION.MIGRATE_ON_STARTUP";
|
||||
|
||||
trait FromConfig: Sized {
|
||||
fn from_config(config: &Config) -> Result<Self, String>;
|
||||
fn validate(&self) -> Result<(), String>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProgramSettings {
|
||||
pub logging: LoggingSettings,
|
||||
pub database: DatabaseSettings,
|
||||
pub server: ServerSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoggingSettings {
|
||||
pub level: Level,
|
||||
pub utc: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DatabaseSettings {
|
||||
pub url: String,
|
||||
pub max_connections: u32,
|
||||
pub migrate_on_startup: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerSettings {
|
||||
pub address: IpAddr,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl FromConfig for ProgramSettings {
|
||||
fn from_config(_config: &Config) -> Result<Self, String> {
|
||||
let config = ProgramSettings {
|
||||
logging: LoggingSettings::from_config(_config)?,
|
||||
database: DatabaseSettings::from_config(_config)?,
|
||||
server: ServerSettings::from_config(_config)?,
|
||||
};
|
||||
config.validate()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
self.logging.validate()?;
|
||||
self.database.validate()?;
|
||||
self.server.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromConfig for LoggingSettings {
|
||||
fn from_config(_config: &Config) -> Result<Self, String> {
|
||||
const DEFAULT_LOGGING_LEVEL: Level = Level::INFO;
|
||||
Ok(LoggingSettings {
|
||||
level: _config
|
||||
.get_string(LOGGING_LEVEL_KEY)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
"Failed to read {} from configuration, defaulting to {}. Error: {}",
|
||||
LOGGING_LEVEL_KEY, DEFAULT_LOGGING_LEVEL, err
|
||||
);
|
||||
DEFAULT_LOGGING_LEVEL.to_string()
|
||||
})
|
||||
.parse()
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
"Invalid logging level in configuration, defaulting to {}. Error: {}",
|
||||
DEFAULT_LOGGING_LEVEL, err
|
||||
);
|
||||
DEFAULT_LOGGING_LEVEL
|
||||
}),
|
||||
utc: _config
|
||||
.get_bool(LOGGING_UTC_KEY)
|
||||
.unwrap_or_else(|err: ConfigError| {
|
||||
const DEFAULT_UTC: bool = false;
|
||||
warn!(
|
||||
"Invalid UTC setting in configuration, defaulting to {}. Error: {}",
|
||||
DEFAULT_UTC, err
|
||||
);
|
||||
DEFAULT_UTC
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromConfig for DatabaseSettings {
|
||||
fn from_config(_config: &Config) -> Result<Self, String> {
|
||||
Ok(DatabaseSettings {
|
||||
url: _config
|
||||
.get_string(DATABASE_URL_KEY)
|
||||
.map_err(|op| match op {
|
||||
ConfigError::NotFound(_) => "Database URL not found in configuration".into(),
|
||||
err => {
|
||||
format!("Failed to read Database URL from configuration {err}")
|
||||
}
|
||||
})?,
|
||||
max_connections: _config
|
||||
.get_int(DATABASE_MAX_CONNECTIONS_KEY)
|
||||
.unwrap_or_else(|err| {
|
||||
const DEFAULT_MAX_CONNECTIONS: i64 = 10;
|
||||
warn!(
|
||||
"{} not set or invalid in configuration, defaulting to {}. Error: {}",
|
||||
DATABASE_MAX_CONNECTIONS_KEY, DEFAULT_MAX_CONNECTIONS, err
|
||||
);
|
||||
DEFAULT_MAX_CONNECTIONS
|
||||
}) as u32,
|
||||
migrate_on_startup: _config
|
||||
.get_bool(DATABASE_MIGRATE_ON_STARTUP_KEY)
|
||||
.unwrap_or_else(|err| {
|
||||
const DEFAULT_MIGRATE_ON_STARTUP: bool = true;
|
||||
warn!(
|
||||
"{} not set or invalid in configuration, defaulting to {}. Error: {}",
|
||||
DATABASE_MIGRATE_ON_STARTUP_KEY, DEFAULT_MIGRATE_ON_STARTUP, err
|
||||
);
|
||||
DEFAULT_MIGRATE_ON_STARTUP
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromConfig for ServerSettings {
|
||||
fn from_config(_config: &Config) -> Result<Self, String> {
|
||||
Ok(ServerSettings {
|
||||
address: _config
|
||||
.get_string(SERVER_ADDRESS_KEY)
|
||||
.unwrap_or_else(|err| {
|
||||
const DEFAULT_ADDRESS: &str = "0.0.0.0";
|
||||
match err {
|
||||
ConfigError::NotFound(_) => {}
|
||||
_ => {
|
||||
warn!(
|
||||
"Failed to read {} from configuration, defaulting to {}. Error: {}",
|
||||
SERVER_ADDRESS_KEY, DEFAULT_ADDRESS, err
|
||||
);
|
||||
}
|
||||
};
|
||||
DEFAULT_ADDRESS.to_string()
|
||||
})
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid {} in configuration: {}", SERVER_ADDRESS_KEY, e))?,
|
||||
|
||||
port: _config.get_int(SERVER_PORT_KEY).unwrap_or_else(|err| {
|
||||
const DEFAULT_PORT: i64 = 8080;
|
||||
warn!(
|
||||
"{} not set or invalid in configuration, defaulting to {}. Error: {}",
|
||||
SERVER_PORT_KEY, DEFAULT_PORT, err
|
||||
);
|
||||
DEFAULT_PORT
|
||||
}) as u16,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
#[allow(clippy::absurd_extreme_comparisons, unused_comparisons)]
|
||||
if self.port == 0 || self.port > 65535 {
|
||||
return Err("Server port must be between 1 and 65535".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_program_settings() -> ProgramSettings {
|
||||
debug!("Loading program settings from configuration sources");
|
||||
let settings = Config::builder()
|
||||
// dev / generated config has the highest priority (Overwrite by user config files)
|
||||
.add_source(config::File::with_name("generated-config.yaml").required(false))
|
||||
// user config files
|
||||
.add_source(
|
||||
config::File::with_name("/etc/yet-another-nginx-proxy-manager/config").required(false),
|
||||
)
|
||||
.add_source(
|
||||
config::File::with_name("$HOME/.config/yet-another-nginx-proxy-manager/config")
|
||||
.required(false),
|
||||
)
|
||||
.add_source(config::File::with_name("config.yaml").required(false))
|
||||
// environment variables have the highest priority (Overwrite all config files)
|
||||
.add_source(
|
||||
config::Environment::with_prefix("YANPM")
|
||||
.separator("__")
|
||||
.prefix_separator("_"),
|
||||
)
|
||||
.build()
|
||||
.expect("Failed to build configuration");
|
||||
|
||||
debug!("Configuration sources loaded successfully");
|
||||
debug!("Parsing program settings from configuration");
|
||||
|
||||
ProgramSettings::from_config(&settings)
|
||||
.inspect_err(|err| {
|
||||
error!("Configuration error: {}", err);
|
||||
debug!("Current configurations: {:#?}", settings);
|
||||
})
|
||||
.inspect(|_| {
|
||||
debug!("Program settings parsed successfully");
|
||||
})
|
||||
.expect("Failed to load program settings from configuration")
|
||||
}
|
||||
113
apps/api/src/main.rs
Normal file
113
apps/api/src/main.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
mod config;
|
||||
mod tasks;
|
||||
|
||||
use axum::Router;
|
||||
use database::{ConnectOptions, get_connection};
|
||||
use tracing::{debug, info};
|
||||
use tracing_subscriber::fmt::format::{DefaultFields, Format};
|
||||
|
||||
use crate::config::{LoggingSettings, ProgramSettings, get_program_settings};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Temporary subscriber for initial logging during configuration reading
|
||||
let make_temporary_subscriber = || {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.with_target(false)
|
||||
.with_level(true)
|
||||
.finish()
|
||||
};
|
||||
|
||||
let settings =
|
||||
tracing::subscriber::with_default(make_temporary_subscriber(), || -> ProgramSettings {
|
||||
debug!("Temporary subscriber installed.");
|
||||
info!("Reading configuration...");
|
||||
let settings = get_program_settings();
|
||||
info!("Configuration read successfully.");
|
||||
debug!("Resetting global subscriber...");
|
||||
|
||||
let subscriber = get_global_tracing_subscriber_builder(&settings.logging).finish();
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("Failed to set global default subscriber");
|
||||
|
||||
debug!(
|
||||
"Global subscriber set with logging level: {:?}",
|
||||
settings.logging.level
|
||||
);
|
||||
|
||||
settings
|
||||
});
|
||||
|
||||
tasks::startup::run_startup_tasks(&settings)
|
||||
.await
|
||||
.expect("Failed to run startup tasks");
|
||||
|
||||
// setup database connection pool
|
||||
info!("Establishing database connection...");
|
||||
debug!("Database URL: {}", settings.database.url);
|
||||
|
||||
let db_options = |options: &mut ConnectOptions| {
|
||||
options.max_connections(settings.database.max_connections);
|
||||
};
|
||||
|
||||
let db_connection = get_connection(&settings.database.url, Some(db_options))
|
||||
.await
|
||||
.expect("Failed to establish database connection");
|
||||
|
||||
info!("Database connection established.");
|
||||
|
||||
// build the axum app and run the server...
|
||||
info!("Starting application...");
|
||||
let app: Router = Router::new();
|
||||
let address = format!("{}:{}", settings.server.address, settings.server.port);
|
||||
info!("Starting server at http://{}", address);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(address)
|
||||
.await
|
||||
.expect("Failed to bind to address");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("Failed to run the server");
|
||||
}
|
||||
|
||||
fn get_global_tracing_subscriber_builder(
|
||||
settings: &LoggingSettings,
|
||||
) -> tracing_subscriber::fmt::SubscriberBuilder<
|
||||
DefaultFields,
|
||||
Format<tracing_subscriber::fmt::format::Full, BoxedTimer>,
|
||||
> {
|
||||
// After configuration is read, install the global subscriber
|
||||
let builder = tracing_subscriber::fmt()
|
||||
.with_max_level(settings.level)
|
||||
.with_target(false)
|
||||
.with_level(true);
|
||||
|
||||
if settings.utc {
|
||||
builder.with_timer(BoxedTimer(Box::new(
|
||||
tracing_subscriber::fmt::time::UtcTime::rfc_3339(),
|
||||
)))
|
||||
} else {
|
||||
builder.with_timer(BoxedTimer(Box::new(
|
||||
tracing_subscriber::fmt::time::ChronoLocal::rfc_3339(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// A small wrapper that holds a boxed `FormatTime` trait object and itself
|
||||
// implements `FormatTime`, allowing us to use it as a concrete type with
|
||||
// `builder.with_timer` while still picking the concrete timer implementation
|
||||
// at runtime.
|
||||
// wrapper type to hold boxed timers and implement the `FormatTime` trait for
|
||||
// a concrete type so `with_timer` may be called once outside the conditional.
|
||||
struct BoxedTimer(Box<dyn tracing_subscriber::fmt::time::FormatTime + Send + Sync + 'static>);
|
||||
|
||||
impl tracing_subscriber::fmt::time::FormatTime for BoxedTimer {
|
||||
fn format_time(
|
||||
&self,
|
||||
w: &mut tracing_subscriber::fmt::format::Writer<'_>,
|
||||
) -> std::result::Result<(), std::fmt::Error> {
|
||||
self.0.format_time(w)
|
||||
}
|
||||
}
|
||||
1
apps/api/src/tasks.rs
Normal file
1
apps/api/src/tasks.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod startup;
|
||||
25
apps/api/src/tasks/startup.rs
Normal file
25
apps/api/src/tasks/startup.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use migration::migrate_database;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::config::ProgramSettings;
|
||||
|
||||
pub async fn run_startup_tasks(config: &ProgramSettings) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Here you can add any startup tasks you want to run when the application starts.
|
||||
info!("Running startup tasks...");
|
||||
if config.database.migrate_on_startup {
|
||||
run_database_migrations(&config.database.url).await?;
|
||||
} else {
|
||||
info!("Database migration on startup is disabled. Skipping migration.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_database_migrations(db_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Logic to run database migrations
|
||||
info!("Running database migrations...");
|
||||
debug!("Database URL: {}", db_url);
|
||||
migrate_database(db_url).await.map_err(Box::new)?;
|
||||
info!("Database migrations completed.");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user