From 7c2274ab2bb9efd091ee98b1ffdbe75a8588119f Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 23 Oct 2024 16:07:06 -0700 Subject: [PATCH] Move to clap derive (#15) Co-authored-by: jking-aus <72330194+jking-aus@users.noreply.github.com> Co-authored-by: Age Manning --- Cargo.lock | 59 ++++++ Cargo.toml | 3 +- anchor/client/Cargo.toml | 1 + anchor/client/src/cli.rs | 372 ++++++++++++++++++++---------------- anchor/client/src/config.rs | 94 +++------ anchor/client/src/lib.rs | 2 +- anchor/src/main.rs | 8 +- 7 files changed, 302 insertions(+), 237 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bf0bbb..f0a6429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -585,6 +585,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -597,6 +598,19 @@ dependencies = [ "anstyle", "clap_lex", "strsim 0.11.1", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.79", ] [[package]] @@ -619,6 +633,7 @@ dependencies = [ "regex", "sensitive_url", "serde", + "strum", "target_info", "task_executor", "tracing", @@ -1368,6 +1383,18 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2719,6 +2746,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2820,6 +2869,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "terminal_size" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "test_random_derive" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index c650c26..0f40aea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,12 +18,13 @@ slot_clock = { git = "https://github.com/sigp/lighthouse", branch = "anchor" } derive_more = { version = "1.0.0", features = ["full"] } async-channel = "1.9" axum = "0.7.7" -clap = "4.5.15" +clap = { version = "4.5.15", features = ["derive", "wrap_help"]} dirs = "5.0.1" futures = "0.3.30" # dirs = "3" hyper = "1.4" serde = { version = "1.0.208", features = ["derive"] } +strum = { version = "0.24", features = ["derive"] } tokio = { version = "1.39.2", features = [ "rt", "rt-multi-thread", diff --git a/anchor/client/Cargo.toml b/anchor/client/Cargo.toml index eeda165..0a6aa32 100644 --- a/anchor/client/Cargo.toml +++ b/anchor/client/Cargo.toml @@ -13,6 +13,7 @@ task_executor = { workspace = true } http_api = { workspace = true } clap = { workspace = true } serde = { workspace = true } +strum = { workspace = true } sensitive_url = { workspace = true } dirs = { workspace = true } hyper = { workspace = true } diff --git a/anchor/client/src/cli.rs b/anchor/client/src/cli.rs index 61a705d..cad3677 100644 --- a/anchor/client/src/cli.rs +++ b/anchor/client/src/cli.rs @@ -1,8 +1,13 @@ use clap::builder::styling::*; -use clap::{builder::ArgPredicate, Arg, ArgAction, Command}; +use clap::builder::ArgPredicate; +use clap::{Parser, ValueEnum}; +use serde::{Deserialize, Serialize}; +use strum::Display; // use clap_utils::{get_color_style, FLAG_HEADER}; use crate::version::VERSION; use ethereum_hashing::have_sha_extensions; +use std::net::IpAddr; +use std::path::PathBuf; use std::sync::LazyLock; pub static SHORT_VERSION: LazyLock = LazyLock::new(|| VERSION.replace("Anchor/", "")); @@ -40,168 +45,209 @@ fn build_profile_name() -> String { .to_string() } -pub fn cli_app() -> Command { - Command::new("anchor") - .version(SHORT_VERSION.as_str()) - .author("Sigma Prime ") - .styles(get_color_style()) - .next_line_help(true) - .term_width(80) - .about( - "Anchor is a rust-based SSV client. Currently under active developement and should NOT be used for production." - ) - .long_version(LONG_VERSION.as_str()) - .display_order(0) - .arg( - Arg::new("debug-level") - .long("debug-level") - .value_name("LEVEL") - .help("Specifies the verbosity level used when emitting logs to the terminal.") - .action(ArgAction::Set) - .value_parser(["info", "debug", "trace", "warn", "error"]) - .global(true) - .default_value("info") - .display_order(0) - ) - .arg( - Arg::new("datadir") - .long("datadir") - .short('d') - .value_name("DIR") - .global(true) - .help( - "Used to specify a custom root data directory for anchor keys and databases. \ - Defaults to $HOME/.anchor/{network} where network is the value of the `network` flag \ - Note: Users should specify separate custom datadirs for different networks.") - .action(ArgAction::Set) - .display_order(0) - ) - /* External APIs */ - .arg( - Arg::new("beacon-nodes") - .long("beacon-nodes") - .value_name("NETWORK_ADDRESSES") - .help("Comma-separated addresses to one or more beacon node HTTP APIs. \ - Default is http://localhost:5052." - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("execution-nodes") - .long("beacon-nodes") - .value_name("NETWORK_ADDRESSES") - .help("Comma-separated addresses to one or more beacon node HTTP APIs. \ - Default is http://localhost:8545." - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("beacon-nodes-tls-certs") - .long("beacon-nodes-tls-certs") - .value_name("CERTIFICATE-FILES") - .action(ArgAction::Set) - .help("Comma-separated paths to custom TLS certificates to use when connecting \ - to a beacon node (and/or proposer node). These certificates must be in PEM format and are used \ - in addition to the OS trust store. Commas must only be used as a \ - delimiter, and must not be part of the certificate path.") - .display_order(0) - ) - .arg( - Arg::new("execution-nodes-tls-certs") - .long("execution-nodes-tls-certs") - .value_name("CERTIFICATE-FILES") - .action(ArgAction::Set) - .help("Comma-separated paths to custom TLS certificates to use when connecting \ - to an exection node. These certificates must be in PEM format and are used \ - in addition to the OS trust store. Commas must only be used as a \ - delimiter, and must not be part of the certificate path.") - .display_order(0) - ) - /* REST API related arguments */ - .arg( - Arg::new("http") - .long("http") - .help("Enable the RESTful HTTP API server. Disabled by default.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - /* - * Note: The HTTP server is **not** encrypted (i.e., not HTTPS) and therefore it is - * unsafe to publish on a public network. - * - * If the `--http-address` flag is used, the `--unencrypted-http-transport` flag - * must also be used in order to make it clear to the user that this is unsafe. - */ - .arg( - Arg::new("http-address") - .long("http-address") - .requires("http") - .value_name("ADDRESS") - .help("Set the address for the HTTP address. The HTTP server is not encrypted \ - and therefore it is unsafe to publish on a public network. When this \ - flag is used, it additionally requires the explicit use of the \ - `--unencrypted-http-transport` flag to ensure the user is aware of the \ - risks involved. For access via the Internet, users should apply \ - transport-layer security like a HTTPS reverse-proxy or SSH tunnelling.") - .requires("unencrypted-http-transport") - .display_order(0) - ) - .arg( - Arg::new("http-port") - .long("http-port") - .requires("http") - .value_name("PORT") - .help("Set the listen TCP port for the RESTful HTTP API server.") - .default_value_if("http", ArgPredicate::IsPresent, "5062") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("http-allow-origin") - .long("http-allow-origin") - .requires("http") - .value_name("ORIGIN") - .help("Set the value of the Access-Control-Allow-Origin response HTTP header. \ - Use * to allow any origin (not recommended in production). \ - If no value is supplied, the CORS allowed origin is set to the listen \ - address of this server (e.g., http://localhost:5062).") - .action(ArgAction::Set) - .display_order(0) - ) - /* Prometheus metrics HTTP server related arguments */ - .arg( - Arg::new("metrics") - .long("metrics") - .help("Enable the Prometheus metrics HTTP server. Disabled by default.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("metrics-address") - .long("metrics-address") - .requires("metrics") - .value_name("ADDRESS") - .help("Set the listen address for the Prometheus metrics HTTP server.") - .default_value_if("metrics", ArgPredicate::IsPresent, "127.0.0.1") - .action(ArgAction::Set) - .display_order(0) - .hide(true) - ) - .arg( - Arg::new("metrics-port") - .long("metrics-port") - .requires("metrics") - .value_name("PORT") - .help("Set the listen TCP port for the Prometheus metrics HTTP server.") - .default_value_if("metrics", ArgPredicate::IsPresent, "5064") - .action(ArgAction::Set) - .display_order(0) - .hide(true) - ) +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, Display, ValueEnum)] +pub enum DebugLevel { + #[strum(serialize = "info")] + Info, + #[strum(serialize = "debug")] + Debug, + #[strum(serialize = "trace")] + Trace, + #[strum(serialize = "warn")] + Warn, + #[strum(serialize = "error")] + Error, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + name = "ssv", + about = "SSV Validator client. Maintained by Sigma Prime.", + author = "Sigma Prime ", + long_version = LONG_VERSION.as_str(), + version = SHORT_VERSION.as_str(), + styles = get_color_style(), + disable_help_flag = true, + next_line_help = true, + term_width = 80, + display_order = 0, +)] +pub struct Anchor { + #[clap( + long, + value_name = "LEVEL", + help = "Specifies the verbosity level used when emitting logs to the terminal.", + default_value_t = DebugLevel::Info, + display_order = 0, + )] + pub debug_level: DebugLevel, + + #[clap( + long, + short = 'd', + global = true, + value_name = "DIR", + help = "Used to specify a custom root data directory for lighthouse keys and databases. \ + Defaults to $HOME/.lighthouse/{network} where network is the value of the `network` flag \ + Note: Users should specify separate custom datadirs for different networks.", + display_order = 0 + )] + pub datadir: Option, + + #[clap( + long, + value_name = "DIR", + help = "The directory which contains the password to unlock the validator \ + voting keypairs. Each password should be contained in a file where the \ + name is the 0x-prefixed hex representation of the validators voting public \ + key. Defaults to ~/.lighthouse/{network}/secrets.", + conflicts_with = "datadir", + display_order = 0 + )] + pub secrets_dir: Option, + + /* External APIs */ + #[clap( + long, + value_name = "NETWORK_ADDRESSES", + help = "Comma-separated addresses to one or more beacon node HTTP APIs. \ + Default is http://localhost:5052.", + display_order = 0 + )] + pub beacon_nodes: Option>, + + #[clap( + long, + value_name = "NETWORK_ADDRESSES", + help = "Comma-separated addresses to one or more beacon node HTTP APIs. \ + Default is http://localhost:8545.", + display_order = 0 + )] + pub execution_nodes: Option>, + + #[clap( + long, + value_name = "CERTIFICATE-FILES", + help = "Comma-separated paths to custom TLS certificates to use when connecting \ + to a beacon node (and/or proposer node). These certificates must be in PEM format and are used \ + in addition to the OS trust store. Commas must only be used as a \ + delimiter, and must not be part of the certificate path.", + display_order = 0 + )] + pub beacon_nodes_tls_certs: Option>, + + #[clap( + long, + value_name = "CERTIFICATE-FILES", + help = "Comma-separated paths to custom TLS certificates to use when connecting \ + to an exection node. These certificates must be in PEM format and are used \ + in addition to the OS trust store. Commas must only be used as a \ + delimiter, and must not be part of the certificate path", + display_order = 0 + )] + pub execution_nodes_tls_certs: Option>, + + /* REST API related arguments */ + #[clap( + long, + help = "Enable the RESTful HTTP API server. Disabled by default.", + help_heading = FLAG_HEADER, + display_order = 0, + )] + pub http: bool, + + /* + * Note: The HTTP server is **not** encrypted (i.e., not HTTPS) and therefore it is + * unsafe to publish on a public network. + * + * If the `--http-address` flag is used, the `--unencrypted-http-transport` flag + * must also be used in order to make it clear to the user that this is unsafe. + */ + #[clap( + long, + value_name = "ADDRESS", + help = "Set the address for the HTTP address. The HTTP server is not encrypted \ + and therefore it is unsafe to publish on a public network. When this \ + flag is used, it additionally requires the explicit use of the \ + `--unencrypted-http-transport` flag to ensure the user is aware of the \ + risks involved. For access via the Internet, users should apply \ + transport-layer security like a HTTPS reverse-proxy or SSH tunnelling.", + display_order = 0, + requires = "http", + requires = "unencrypted_http_transport" + )] + pub http_address: Option, + + #[clap( + long, + help = "This is a safety flag to ensure that the user is aware that the http \ + transport is unencrypted and using a custom HTTP address is unsafe.", + display_order = 0, + requires = "http_address", + help_heading = FLAG_HEADER, + )] + pub unencrypted_http_transport: bool, + + #[clap( + long, + value_name = "PORT", + requires = "http", + help = "Set the listen TCP port for the RESTful HTTP API server.", + display_order = 0, + default_value_if("http", ArgPredicate::IsPresent, "5062") + )] + pub http_port: Option, + + #[clap( + long, + value_name = "ORIGIN", + help = "Set the value of the Access-Control-Allow-Origin response HTTP header. \ + Use * to allow any origin (not recommended in production). \ + If no value is supplied, the CORS allowed origin is set to the listen \ + address of this server (e.g., http://localhost:5062).", + display_order = 0, + requires = "http" + )] + pub http_allow_origin: Option, + + /* Prometheus metrics HTTP server related arguments */ + #[clap( + long, + help = "Enable the Prometheus metrics HTTP server. Disabled by default.", + display_order = 0, + help_heading = FLAG_HEADER, + )] + pub metrics: bool, + + #[clap( + long, + value_name = "ADDRESS", + help = "Set the listen address for the Prometheus metrics HTTP server.", + default_value_if("metrics", ArgPredicate::IsPresent, "127.0.0.1"), + display_order = 0, + requires = "metrics" + )] + pub metrics_address: Option, + + #[clap( + long, + value_name = "PORT", + help = "Set the listen TCP port for the Prometheus metrics HTTP server.", + display_order = 0, + default_value_if("metrics", ArgPredicate::IsPresent, "5064"), + requires = "metrics" + )] + pub metrics_port: u16, + + #[clap( + long, + global = true, + help = "Prints help information", + action = clap::ArgAction::HelpLong, + display_order = 0, + help_heading = FLAG_HEADER + )] + help: Option, } pub fn get_color_style() -> Styles { diff --git a/anchor/client/src/config.rs b/anchor/client/src/config.rs index 33130b4..92f5e5b 100644 --- a/anchor/client/src/config.rs +++ b/anchor/client/src/config.rs @@ -1,13 +1,12 @@ // use crate::{http_api, http_metrics}; -use clap::ArgMatches; // use clap_utils::{flags::DISABLE_MALLOC_TUNING_FLAG, parse_optional, parse_required}; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use std::fs; -use std::net::IpAddr; use std::path::PathBuf; -use std::str::FromStr; + +use crate::cli::Anchor; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; pub const DEFAULT_EXECUTION_NODE: &str = "http://localhost:8545/"; @@ -77,7 +76,7 @@ impl Default for Config { /// Returns a `Default` implementation of `Self` with some parameters modified by the supplied /// `cli_args`. -pub fn from_cli(cli_args: &ArgMatches) -> Result { +pub fn from_cli(cli_args: &Anchor) -> Result { let mut config = Config::default(); let default_root_dir = dirs::home_dir() @@ -85,14 +84,14 @@ pub fn from_cli(cli_args: &ArgMatches) -> Result { .unwrap_or_else(|| PathBuf::from(".")); let (mut data_dir, mut secrets_dir) = (None, None); - if cli_args.get_one::("datadir").is_some() { - let temp_data_dir: PathBuf = parse_required(cli_args, "datadir")?; - secrets_dir = Some(temp_data_dir.join(DEFAULT_SECRETS_DIR)); - data_dir = Some(temp_data_dir); - }; - - if cli_args.get_one::("secrets-dir").is_some() { - secrets_dir = Some(parse_required(cli_args, "secrets-dir")?); + + if let Some(datadir) = cli_args.datadir.clone() { + secrets_dir = Some(datadir.join(DEFAULT_SECRETS_DIR)); + data_dir = Some(datadir); + } + + if cli_args.secrets_dir.is_some() { + secrets_dir = cli_args.secrets_dir.clone(); } config.data_dir = data_dir.unwrap_or_else(|| default_root_dir.join(DEFAULT_ROOT_DIR)); @@ -104,43 +103,33 @@ pub fn from_cli(cli_args: &ArgMatches) -> Result { .map_err(|e| format!("Failed to create {:?}: {:?}", config.data_dir, e))?; } - if let Some(beacon_nodes) = parse_optional::(cli_args, "beacon-nodes")? { + if let Some(beacon_nodes) = &cli_args.beacon_nodes { config.beacon_nodes = beacon_nodes - .split(',') - .map(SensitiveUrl::parse) + .iter() + .map(|s| SensitiveUrl::parse(s)) .collect::>() .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?; } - if let Some(execution_nodes) = parse_optional::(cli_args, "execution-nodes")? { + if let Some(execution_nodes) = &cli_args.execution_nodes { config.execution_nodes = execution_nodes - .split(',') - .map(SensitiveUrl::parse) + .iter() + .map(|s| SensitiveUrl::parse(s)) .collect::>() .map_err(|e| format!("Unable to parse execution node URL: {:?}", e))?; } - if let Some(tls_certs) = parse_optional::(cli_args, "beacon-nodes-tls-certs")? { - config.beacon_nodes_tls_certs = Some(tls_certs.split(',').map(PathBuf::from).collect()); - } - - if let Some(tls_certs) = parse_optional::(cli_args, "execution-nodes-tls-certs")? { - config.execution_nodes_tls_certs = Some(tls_certs.split(',').map(PathBuf::from).collect()); - } + config.beacon_nodes_tls_certs = cli_args.beacon_nodes_tls_certs.clone(); + config.execution_nodes_tls_certs = cli_args.execution_nodes_tls_certs.clone(); /* * Http API server */ + config.http_api.enabled = cli_args.http; - if cli_args.get_flag("http") { - config.http_api.enabled = true; - } - - if let Some(address) = cli_args.get_one::("http-address") { - if cli_args.get_flag("unencrypted-http-transport") { - config.http_api.listen_addr = address - .parse::() - .map_err(|_| "http-address is not a valid IP address.")?; + if let Some(address) = cli_args.http_address { + if cli_args.unencrypted_http_transport { + config.http_api.listen_addr = address; } else { return Err( "While using `--http-address`, you must also use `--unencrypted-http-transport`." @@ -149,13 +138,11 @@ pub fn from_cli(cli_args: &ArgMatches) -> Result { } } - if let Some(port) = cli_args.get_one::("http-port") { - config.http_api.listen_port = port - .parse::() - .map_err(|_| "http-port is not a valid u16.")?; + if let Some(port) = cli_args.http_port { + config.http_api.listen_port = port; } - if let Some(allow_origin) = cli_args.get_one::("http-allow-origin") { + if let Some(allow_origin) = &cli_args.http_allow_origin { // Pre-validate the config value to give feedback to the user on node startup, instead of // as late as when the first API response is produced. hyper::header::HeaderValue::from_str(allow_origin) @@ -203,35 +190,6 @@ pub fn from_cli(cli_args: &ArgMatches) -> Result { Ok(config) } -// Helper functions for handling CLAP arguments - -/// Returns the value of `name` or an error if it is not in `matches` or does not parse -/// successfully using `std::string::FromStr`. -pub fn parse_required(matches: &ArgMatches, name: &str) -> Result -where - T: FromStr, - ::Err: std::fmt::Display, -{ - parse_optional(matches, name)?.ok_or_else(|| format!("{} not specified", name)) -} - -/// Returns the value of `name` (if present) or an error if it does not parse successfully using -/// `std::string::FromStr`. -pub fn parse_optional(matches: &ArgMatches, name: &str) -> Result, String> -where - T: FromStr, - ::Err: std::fmt::Display, -{ - matches - .try_get_one::(name) - .map_err(|e| format!("Unable to parse {}: {}", name, e))? - .map(|val| { - val.parse() - .map_err(|e| format!("Unable to parse {}: {}", name, e)) - }) - .transpose() -} - #[cfg(test)] mod tests { use super::*; diff --git a/anchor/client/src/lib.rs b/anchor/client/src/lib.rs index 28568e0..cbc9d27 100644 --- a/anchor/client/src/lib.rs +++ b/anchor/client/src/lib.rs @@ -4,7 +4,7 @@ mod cli; pub mod config; mod version; -pub use cli::cli_app; +pub use cli::Anchor; use config::Config; use task_executor::TaskExecutor; use tracing::{debug, error, info}; diff --git a/anchor/src/main.rs b/anchor/src/main.rs index b817f66..0f4bc07 100644 --- a/anchor/src/main.rs +++ b/anchor/src/main.rs @@ -1,7 +1,8 @@ +use clap::Parser; use tracing::{error, info}; mod environment; -use client::Client; +use client::{config, Anchor, Client}; use environment::Environment; use task_executor::ShutdownReason; @@ -12,13 +13,12 @@ fn main() { } // Obtain the CLI and build the config - let cli = client::cli_app(); - let matches = cli.get_matches(); + let anchor_config: Anchor = Anchor::parse(); // Currently the only binary is the client. We build the client config, but later this will // generalise to other sub commands // Build the client config - let config = match client::config::from_cli(&matches) { + let config = match config::from_cli(&anchor_config) { Ok(config) => config, Err(e) => { error!(e, "Unable to initialize configuration");