use clap::{Parser, Subcommand};
use lloggs::{LoggingArgs, PreArgs, WorkerGuard};
use miette::{Result, miette};
use tracing::debug;
#[derive(Debug, Clone, Parser)]
pub struct Args {
#[command(flatten)]
logging: LoggingArgs,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Clone, Parser)]
struct DaemonArgs {
#[arg(long, env = "DATABASE_URL")]
database_url: Option<String>,
#[arg(long)]
glob: Vec<String>,
#[arg(long, env = "EMAIL_FROM")]
email_from: Option<String>,
#[arg(long, env = "MAILGUN_API_KEY")]
mailgun_api_key: Option<String>,
#[arg(long, env = "MAILGUN_DOMAIN")]
mailgun_domain: Option<String>,
#[arg(long, env = "TAMANU_VERSION", default_value = "0.0.0")]
tamanu_version: String,
#[arg(long, env = "DEVICE_KEY_FILE")]
device_key_file: Option<std::path::PathBuf>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_server: bool,
#[arg(long)]
server_addr: Vec<std::net::SocketAddr>,
#[arg(long, default_value = "600")]
watchdog_timeout: u64,
#[arg(long)]
no_watchdog: bool,
}
#[derive(Debug, Clone, Subcommand)]
enum Command {
Run {
#[command(flatten)]
daemon: DaemonArgs,
},
Status {
#[arg(long)]
server_addr: Vec<std::net::SocketAddr>,
},
Reload {
#[arg(long)]
server_addr: Vec<std::net::SocketAddr>,
},
LoadedAlerts {
#[arg(long)]
server_addr: Vec<std::net::SocketAddr>,
#[arg(long)]
detail: bool,
},
PauseAlert {
alert: String,
#[arg(long)]
until: Option<String>,
#[arg(long)]
server_addr: Vec<std::net::SocketAddr>,
},
Validate {
file: std::path::PathBuf,
#[arg(long)]
server_addr: Vec<std::net::SocketAddr>,
},
#[cfg(windows)]
Install,
#[cfg(windows)]
Uninstall,
#[cfg(windows)]
ConfigureRecovery,
#[cfg(windows)]
#[command(hide = true)]
Service {
#[command(flatten)]
daemon: DaemonArgs,
},
#[command(hide = true, name = "_docs")]
Docs,
}
fn get_args() -> Result<(Args, WorkerGuard)> {
let log_guard = PreArgs::parse().setup().map_err(|err| miette!("{err}"))?;
debug!("parsing arguments");
let args = Args::parse();
let log_guard = match log_guard {
Some(g) => g,
None => args
.logging
.setup(|v| match v {
0 => "bestool_alertd=info",
1 => "info,bestool_alertd=debug",
2 => "debug",
3 => "debug,bestool_alertd=trace",
_ => "trace",
})
.map_err(|err| miette!("{err}"))?,
};
debug!(?args, "got arguments");
Ok((args, log_guard))
}
async fn build_daemon_config(daemon: DaemonArgs) -> Result<bestool_alertd::DaemonConfig> {
let database_url = daemon
.database_url
.ok_or_else(|| miette!("--database-url is required"))?;
if daemon.glob.is_empty() {
return Err(miette!("at least one --glob must be specified"));
}
let email = match (
daemon.email_from,
daemon.mailgun_api_key,
daemon.mailgun_domain,
) {
(Some(from), Some(api_key), Some(domain)) => Some(bestool_alertd::EmailConfig {
from,
mailgun_api_key: api_key,
mailgun_domain: domain,
}),
(None, None, None) => None,
_ => {
return Err(miette!(
"either provide all email options (--email-from, --mailgun-api-key, --mailgun-domain) or none"
));
}
};
let watchdog_timeout = if daemon.no_watchdog {
None
} else {
Some(std::time::Duration::from_secs(daemon.watchdog_timeout))
};
let device_key_pem = if let Some(path) = daemon.device_key_file {
Some(
std::fs::read_to_string(&path)
.map_err(|err| miette!("reading device key file {}: {err}", path.display()))?,
)
} else {
None
};
let pg_pool = bestool_postgres::pool::create_pool(&database_url, "bestool-alertd").await?;
let mut daemon_config = bestool_alertd::DaemonConfig::new(
daemon.glob,
pg_pool,
database_url,
daemon.tamanu_version,
)
.with_dry_run(daemon.dry_run)
.with_no_server(daemon.no_server)
.with_server_addrs(daemon.server_addr)
.with_watchdog_timeout(watchdog_timeout);
if let Some(email) = email {
daemon_config = daemon_config.with_email(email);
}
if let Some(pem) = device_key_pem {
daemon_config = daemon_config.with_device_key_pem(pem);
}
Ok(daemon_config)
}
async fn run_daemon(daemon: DaemonArgs) -> Result<()> {
let daemon_config = build_daemon_config(daemon).await?;
bestool_alertd::run(daemon_config).await
}
#[tokio::main]
async fn main() -> Result<()> {
let (args, _guard) = get_args()?;
match args.command {
Command::Run { daemon } => run_daemon(daemon).await,
Command::Status { server_addr } => {
let addrs = if server_addr.is_empty() {
bestool_alertd::commands::default_server_addrs()
} else {
server_addr
};
bestool_alertd::commands::get_status(&addrs).await
}
Command::Reload { server_addr } => {
let addrs = if server_addr.is_empty() {
bestool_alertd::commands::default_server_addrs()
} else {
server_addr
};
bestool_alertd::commands::send_reload(&addrs).await
}
Command::LoadedAlerts {
server_addr,
detail,
} => {
let addrs = if server_addr.is_empty() {
bestool_alertd::commands::default_server_addrs()
} else {
server_addr
};
bestool_alertd::commands::get_loaded_alerts(&addrs, detail).await
}
Command::PauseAlert {
alert,
until,
server_addr,
} => {
let addrs = if server_addr.is_empty() {
bestool_alertd::commands::default_server_addrs()
} else {
server_addr
};
bestool_alertd::commands::pause_alert(&alert, until.as_deref(), &addrs).await
}
Command::Validate { file, server_addr } => {
let addrs = if server_addr.is_empty() {
bestool_alertd::commands::default_server_addrs()
} else {
server_addr
};
bestool_alertd::commands::validate_alert(&file, &addrs).await
}
#[cfg(windows)]
Command::Install => bestool_alertd::windows_service::install_service(),
#[cfg(windows)]
Command::Uninstall => bestool_alertd::windows_service::uninstall_service(),
#[cfg(windows)]
Command::ConfigureRecovery => bestool_alertd::windows_service::configure_recovery(),
#[cfg(windows)]
Command::Service { daemon } => {
let daemon_config = build_daemon_config(daemon).await?;
bestool_alertd::windows_service::run_service(daemon_config)
}
Command::Docs => {
let markdown = clap_markdown::help_markdown::<Args>();
println!("{}", markdown);
Ok(())
}
}
}