From dee7dbec8fa5c598bb7d85c14b99ef36623bb73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20Br=C3=A9ard?= Date: Sat, 21 Dec 2024 17:54:00 +0100 Subject: [PATCH] Parce the command line arguments --- Cargo.lock | 157 +++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/cli.rs | 259 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/log.rs | 30 ++++++ src/main.rs | 21 +++-- 5 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/log.rs diff --git a/Cargo.lock b/Cargo.lock index 3c00530..0bac9fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 3 name = "acmed" version = "0.25.0-dev" dependencies = [ + "clap", "tokio", "tracing", "tracing-subscriber", @@ -26,6 +27,55 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -47,12 +97,69 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "lazy_static" version = "1.5.0" @@ -117,6 +224,24 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -132,6 +257,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -183,6 +319,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "winapi" version = "0.3.9" @@ -205,6 +353,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 589fb0b..d2b7544 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ ed25519 = [] ed448 = [] [dependencies] +clap = { version = "4.5.23", default-features = false, features = ["color", "derive", "help", "std"] } tokio = { version = "1.42.0", default-features = false, features = ["rt", "rt-multi-thread"] } tracing = { version = "0.1.41", default-features = false, features = ["std"] } tracing-subscriber = { version = "0.3.19", default-features = false, features = ["ansi", "fmt", "std"] } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..9ffd330 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,259 @@ +use crate::log::Level; +use clap::{Args, Parser}; +use std::path::{Path, PathBuf}; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// Path to the main configuration file + #[arg(short, long, value_name = "FILE", default_value = crate::DEFAULT_CONFIG_PATH)] + pub config: PathBuf, + + /// Specify the log level + #[arg(long, value_name = "LEVEL", value_enum, default_value_t = crate::DEFAULT_LOG_LEVEL)] + pub log_level: Level, + + #[command(flatten)] + pub log: Log, + + /// Runs in the foreground + #[arg(short, long, default_value_t = false)] + pub foreground: bool, + + #[command(flatten)] + pub pid: Pid, + + /// Add a root certificate to the trust store (can be set multiple times) + #[arg(long, value_name = "FILE")] + pub root_cert: Vec, +} + +#[derive(Args, Debug)] +#[group(multiple = false)] +pub struct Log { + /// Sends log messages via syslog + #[arg(long)] + pub log_syslog: bool, + + /// Prints log messages to the standard error output + #[arg(long)] + pub log_stderr: bool, +} + +#[derive(Args, Debug)] +#[group(multiple = false)] +pub struct Pid { + /// Path to the PID file + #[arg(long, value_name = "FILE", default_value = crate::DEFAULT_PID_FILE)] + pid_file: PathBuf, + + /// Do not create any PID file + #[arg(long)] + no_pid_file: bool, +} + +impl Pid { + pub fn get_pid_file(&self) -> Option<&Path> { + if !self.no_pid_file { + Some(self.pid_file.as_path()) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn verify_cli() { + CliArgs::command().debug_assert(); + } + + #[test] + fn no_args() { + let args: &[&str] = &[]; + let pa = CliArgs::try_parse_from(args).unwrap(); + assert_eq!(pa.config, PathBuf::from(crate::DEFAULT_CONFIG_PATH)); + assert_eq!(pa.log_level, Level::Warn); + assert_eq!(pa.log.log_syslog, false); + assert_eq!(pa.log.log_stderr, false); + assert_eq!(pa.foreground, false); + assert_eq!(pa.pid.pid_file, PathBuf::from(crate::DEFAULT_PID_FILE)); + assert_eq!(pa.pid.no_pid_file, false); + assert_eq!( + pa.pid.get_pid_file(), + Some(PathBuf::from(crate::DEFAULT_PID_FILE).as_path()) + ); + assert!(pa.root_cert.is_empty()); + } + + #[test] + fn all_args_long_1() { + let argv: &[&str] = &[ + "acmed", + "--config", + "/tmp/test.toml", + "--log-level", + "debug", + "--log-syslog", + "--foreground", + "--pid-file", + "/tmp/debug/acmed.pid", + "--root-cert", + "/tmp/certs/root_01.pem", + "--root-cert", + "/tmp/certs/root_02.pem", + "--root-cert", + "/tmp/certs/root_03.pem", + ]; + let pa = CliArgs::try_parse_from(argv).unwrap(); + assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); + assert_eq!(pa.log_level, Level::Debug); + assert_eq!(pa.log.log_syslog, true); + assert_eq!(pa.log.log_stderr, false); + assert_eq!(pa.foreground, true); + assert_eq!( + pa.pid.get_pid_file(), + Some(PathBuf::from("/tmp/debug/acmed.pid").as_path()) + ); + assert_eq!( + pa.root_cert, + vec![ + PathBuf::from("/tmp/certs/root_01.pem"), + PathBuf::from("/tmp/certs/root_02.pem"), + PathBuf::from("/tmp/certs/root_03.pem") + ] + ); + } + + #[test] + fn all_args_long_2() { + let argv: &[&str] = &[ + "acmed", + "--config", + "/tmp/test.toml", + "--log-level", + "debug", + "--log-stderr", + "--foreground", + "--no-pid-file", + "--root-cert", + "/tmp/certs/root_01.pem", + "--root-cert", + "/tmp/certs/root_02.pem", + "--root-cert", + "/tmp/certs/root_03.pem", + ]; + let pa = CliArgs::try_parse_from(argv).unwrap(); + assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); + assert_eq!(pa.log_level, Level::Debug); + assert_eq!(pa.log.log_syslog, false); + assert_eq!(pa.log.log_stderr, true); + assert_eq!(pa.foreground, true); + assert_eq!(pa.pid.get_pid_file(), None); + assert_eq!( + pa.root_cert, + vec![ + PathBuf::from("/tmp/certs/root_01.pem"), + PathBuf::from("/tmp/certs/root_02.pem"), + PathBuf::from("/tmp/certs/root_03.pem") + ] + ); + } + + #[test] + fn all_args_short_1() { + let argv: &[&str] = &[ + "acmed", + "-c", + "/tmp/test.toml", + "--log-level", + "debug", + "--log-syslog", + "-f", + "--pid-file", + "/tmp/debug/acmed.pid", + "--root-cert", + "/tmp/certs/root_01.pem", + "--root-cert", + "/tmp/certs/root_02.pem", + "--root-cert", + "/tmp/certs/root_03.pem", + ]; + let pa = CliArgs::try_parse_from(argv).unwrap(); + assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); + assert_eq!(pa.log_level, Level::Debug); + assert_eq!(pa.log.log_syslog, true); + assert_eq!(pa.log.log_stderr, false); + assert_eq!(pa.foreground, true); + assert_eq!( + pa.pid.get_pid_file(), + Some(PathBuf::from("/tmp/debug/acmed.pid").as_path()) + ); + assert_eq!( + pa.root_cert, + vec![ + PathBuf::from("/tmp/certs/root_01.pem"), + PathBuf::from("/tmp/certs/root_02.pem"), + PathBuf::from("/tmp/certs/root_03.pem") + ] + ); + } + + #[test] + fn all_args_short_2() { + let argv: &[&str] = &[ + "acmed", + "-c", + "/tmp/test.toml", + "--log-level", + "debug", + "--log-stderr", + "-f", + "--no-pid-file", + "--root-cert", + "/tmp/certs/root_01.pem", + "--root-cert", + "/tmp/certs/root_02.pem", + "--root-cert", + "/tmp/certs/root_03.pem", + ]; + let pa = CliArgs::try_parse_from(argv).unwrap(); + assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); + assert_eq!(pa.log_level, Level::Debug); + assert_eq!(pa.log.log_syslog, false); + assert_eq!(pa.log.log_stderr, true); + assert_eq!(pa.foreground, true); + assert_eq!(pa.pid.get_pid_file(), None); + assert_eq!( + pa.root_cert, + vec![ + PathBuf::from("/tmp/certs/root_01.pem"), + PathBuf::from("/tmp/certs/root_02.pem"), + PathBuf::from("/tmp/certs/root_03.pem") + ] + ); + } + + #[test] + fn err_log_output() { + let argv: &[&str] = &["acmed", "--log-stderr", "--log-syslog"]; + let pa = CliArgs::try_parse_from(argv); + assert!(pa.is_err()); + } + + #[test] + fn err_pid_file() { + let argv: &[&str] = &[ + "acmed", + "--pid-file", + "/tmp/debug/acmed.pid", + "--no-pid-file", + ]; + let pa = CliArgs::try_parse_from(argv); + assert!(pa.is_err()); + } +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..466cd12 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,30 @@ +use clap::ValueEnum; +use tracing_subscriber::FmtSubscriber; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum Level { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl Level { + fn tracing(&self) -> tracing::Level { + match self { + Self::Error => tracing::Level::ERROR, + Self::Warn => tracing::Level::WARN, + Self::Info => tracing::Level::INFO, + Self::Debug => tracing::Level::DEBUG, + Self::Trace => tracing::Level::TRACE, + } + } +} + +pub fn init(level: Level, is_syslog: bool) { + let subscriber = FmtSubscriber::builder() + .with_max_level(level.tracing()) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); +} diff --git a/src/main.rs b/src/main.rs index 3f83b26..fc2af2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,22 @@ -use tracing::Level; -use tracing_subscriber::FmtSubscriber; +mod cli; +mod log; + +use clap::Parser; pub const APP_THREAD_NAME: &str = "acmed-runtime"; +pub const DEFAULT_CONFIG_PATH: &str = "/etc/acmed/acmed.toml"; +pub const DEFAULT_LOG_LEVEL: log::Level = log::Level::Warn; +pub const DEFAULT_PID_FILE: &str = "/run/acmed.pid"; fn main() { - let subscriber = FmtSubscriber::builder() - .with_max_level(Level::TRACE) - .finish(); - tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + // CLI + let args = cli::CliArgs::parse(); + println!("Debug: args: {args:?}"); + + // Initialize the logging system + log::init(args.log_level, args.log.log_syslog); + + // Starting ACMEd tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_name(APP_THREAD_NAME)