diff --git a/CHANGELOG.md b/CHANGELOG.md index d980f575cc..dc46574a54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,16 @@ ## Unreleased +### FEATURES + +- [ibc-relayer-cli] + - Added `config validate` CLI to Hermes ([#600]) + ### IMPROVEMENTS - Update to `tendermint-rs` v0.20.0 ([#1125]) +[#600]: https://github.com/informalsystems/ibc-rs/issues/600 [#1125]: https://github.com/informalsystems/ibc-rs/issues/1125 diff --git a/relayer-cli/src/application.rs b/relayer-cli/src/application.rs index 2ee8bf7ccc..f24004eeb4 100644 --- a/relayer-cli/src/application.rs +++ b/relayer-cli/src/application.rs @@ -4,12 +4,12 @@ use abscissa_core::terminal::component::Terminal; use abscissa_core::{ application::{self, AppCell}, component::Component, - config, Application, Configurable, FrameworkError, StandardPaths, + config, Application, Configurable, FrameworkError, FrameworkErrorKind, StandardPaths, }; use crate::components::{JsonTracing, PrettyTracing}; use crate::entry::EntryPoint; -use crate::{commands::CliCmd, config::Config}; +use crate::{commands::CliCmd, config::validate_config, config::Config}; /// Application state pub static APPLICATION: AppCell = AppCell::new(); @@ -110,7 +110,11 @@ impl Application for CliApp { fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> { // Configure components self.state.components.after_config(&config)?; + + validate_config(&config) + .map_err(|validation_err| FrameworkErrorKind::ConfigError.context(validation_err))?; self.config = Some(config); + Ok(()) } diff --git a/relayer-cli/src/commands.rs b/relayer-cli/src/commands.rs index dd7118896a..db2d62f4ca 100644 --- a/relayer-cli/src/commands.rs +++ b/relayer-cli/src/commands.rs @@ -16,10 +16,10 @@ use crate::config::Config; use crate::DEFAULT_CONFIG_PATH; use self::{ - create::CreateCmds, keys::KeysCmd, listen::ListenCmd, query::QueryCmd, start::StartCmd, - tx::TxCmd, update::UpdateCmds, upgrade::UpgradeCmds, version::VersionCmd, + config::ConfigCmd, create::CreateCmds, keys::KeysCmd, listen::ListenCmd, + misbehaviour::MisbehaviourCmd, query::QueryCmd, start::StartCmd, tx::TxCmd, update::UpdateCmds, + upgrade::UpgradeCmds, version::VersionCmd, }; -use crate::commands::misbehaviour::MisbehaviourCmd; mod config; mod create; @@ -38,11 +38,6 @@ pub fn default_config_file() -> Option { dirs_next::home_dir().map(|home| home.join(DEFAULT_CONFIG_PATH)) } -// TODO: Re-add the `config` subcommand -// /// The `config` subcommand -// #[options(help = "manipulate the relayer configuration")] -// Config(ConfigCmd), - /// Cli Subcommands #[derive(Command, Debug, Options, Runnable)] pub enum CliCmd { @@ -50,6 +45,10 @@ pub enum CliCmd { #[options(help = "Get usage information")] Help(Help), + /// The `config` subcommand + #[options(help = "Validate Hermes configuration file")] + Config(ConfigCmd), + /// The `keys` subcommand #[options(help = "Manage keys in the relayer for each chain")] Keys(KeysCmd), @@ -68,7 +67,7 @@ pub enum CliCmd { /// The `start` subcommand #[options(help = "Start the relayer in multi-chain mode. \ - Relays packets and channel handshake messages between all chains in the config.")] + Relays packets and open handshake messages between all chains in the config.")] Start(StartCmd), /// The `query` subcommand diff --git a/relayer-cli/src/commands/config/validate.rs b/relayer-cli/src/commands/config/validate.rs index c642beeaae..79c2fd1939 100644 --- a/relayer-cli/src/commands/config/validate.rs +++ b/relayer-cli/src/commands/config/validate.rs @@ -1,6 +1,7 @@ use abscissa_core::{Command, Options, Runnable}; use crate::conclude::Output; +use crate::config; use crate::prelude::*; #[derive(Command, Debug, Options)] @@ -10,10 +11,11 @@ impl Runnable for ValidateCmd { /// Validate the loaded configuration. fn run(&self) { let config = app_config(); - info!("Loaded configuration: {:?}", *config); + trace!("loaded configuration: {:#?}", *config); - // TODO: Validate configuration - - Output::with_success().exit(); + match config::validate_config(&config) { + Ok(_) => Output::success("validation passed successfully").exit(), + Err(e) => Output::error(format!("{}", e)).exit(), + } } } diff --git a/relayer-cli/src/commands/start.rs b/relayer-cli/src/commands/start.rs index 41cce404e7..8086398904 100644 --- a/relayer-cli/src/commands/start.rs +++ b/relayer-cli/src/commands/start.rs @@ -6,6 +6,7 @@ use ibc_relayer::config::Config; use ibc_relayer::supervisor::Supervisor; use crate::conclude::Output; +use crate::config; use crate::prelude::*; #[derive(Clone, Command, Debug, Options)] @@ -15,6 +16,11 @@ impl Runnable for StartCmd { fn run(&self) { let config = app_config(); + // No chain is preconfigured + if config.chains.is_empty() { + Output::error(config::Error::ZeroChains).exit(); + }; + match spawn_supervisor(config.clone()).and_then(|s| { info!("Hermes has started"); s.run() diff --git a/relayer-cli/src/components.rs b/relayer-cli/src/components.rs index 025708caa6..fbb9f81901 100644 --- a/relayer-cli/src/components.rs +++ b/relayer-cli/src/components.rs @@ -1,6 +1,6 @@ use std::io; -use abscissa_core::{Component, FrameworkError}; +use abscissa_core::{Component, FrameworkError, FrameworkErrorKind}; use tracing_subscriber::{ fmt::{ format::{DefaultFields, Format, Full, Json, JsonFields}, @@ -14,6 +14,8 @@ use tracing_subscriber::{ use ibc_relayer::config::GlobalConfig; +use crate::config; + /// Custom types to simplify the `Tracing` definition below type JsonFormatter = TracingFormatter, StdWriter>; type PrettyFormatter = TracingFormatter, StdWriter>; @@ -34,7 +36,7 @@ impl JsonTracing { /// Creates a new [`Tracing`] component #[allow(trivial_casts)] pub fn new(cfg: GlobalConfig) -> Result { - let filter = build_tracing_filter(cfg.log_level); + let filter = build_tracing_filter(cfg.log_level.to_string())?; // Note: JSON formatter is un-affected by ANSI 'color' option. Set to 'false'. let use_color = false; @@ -65,7 +67,7 @@ impl PrettyTracing { /// Creates a new [`Tracing`] component #[allow(trivial_casts)] pub fn new(cfg: GlobalConfig) -> Result { - let filter = build_tracing_filter(cfg.log_level); + let filter = build_tracing_filter(cfg.log_level.to_string())?; // Construct a tracing subscriber with the supplied filter and enable reloading. let builder = FmtSubscriber::builder() @@ -92,12 +94,29 @@ fn enable_ansi() -> bool { atty::is(atty::Stream::Stdout) && atty::is(atty::Stream::Stderr) } -fn build_tracing_filter(log_level: String) -> String { +/// Builds a tracing filter based on the input `log_level`. +/// Enables tracing exclusively for the relayer crates. +/// Returns error if the filter failed to build. +fn build_tracing_filter(log_level: String) -> Result { let target_crates = ["ibc_relayer", "ibc_relayer_cli"]; - target_crates + // SAFETY: unwrap() below works as long as `target_crates` is not empty. + let directive_raw = target_crates .iter() .map(|&c| format!("{}={}", c, log_level)) .reduce(|a, b| format!("{},{}", a, b)) - .unwrap() + .unwrap(); + + // Build the filter directive + match EnvFilter::try_new(directive_raw.clone()) { + Ok(out) => Ok(out), + Err(e) => { + let our_err = config::Error::InvalidLogLevel(log_level, e.to_string()); + eprintln!( + "Unable to initialize Hermes from filter directive {:?}: {}", + directive_raw, e + ); + Err(FrameworkErrorKind::ConfigError.context(our_err).into()) + } + } } diff --git a/relayer-cli/src/config.rs b/relayer-cli/src/config.rs index cf434c7d10..71cc77a35a 100644 --- a/relayer-cli/src/config.rs +++ b/relayer-cli/src/config.rs @@ -4,20 +4,40 @@ //! application's configuration file and/or command-line options //! for specifying it. -use std::path::PathBuf; +use std::collections::BTreeSet; -use abscissa_core::{error::BoxError, EntryPoint, Options}; - -use crate::commands::CliCmd; +use thiserror::Error; +use ibc::ics24_host::identifier::ChainId; pub use ibc_relayer::config::Config; -/// Get the path to configuration file -pub fn config_path() -> Result { - let mut args = std::env::args(); - assert!(args.next().is_some(), "expected one argument but got zero"); - let args = args.collect::>(); - let app = EntryPoint::::parse_args_default(args.as_slice())?; - let config_path = app.config.ok_or("no config file specified")?; - Ok(config_path) +/// Specifies all the possible syntactic errors +/// that a Hermes configuration file could contain. +#[derive(Error, Debug)] +pub enum Error { + /// No chain is configured + #[error("config file does not specify any chain")] + ZeroChains, + + /// The log level is invalid + #[error("config file specifies an invalid log level ('{0}'), caused by: {1}")] + InvalidLogLevel(String, String), + + /// Duplicate chains configured + #[error("config file has duplicate entry for the chain with id {0}")] + DuplicateChains(ChainId), +} + +/// Method for syntactic validation of the input +/// configuration file. +pub fn validate_config(config: &Config) -> Result<(), Error> { + // Check for duplicate chain configuration. + let mut unique_chain_ids = BTreeSet::new(); + for chain_id in config.chains.iter().map(|c| c.id.clone()) { + if !unique_chain_ids.insert(chain_id.clone()) { + return Err(Error::DuplicateChains(chain_id)); + } + } + + Ok(()) } diff --git a/relayer/src/config.rs b/relayer/src/config.rs index 87cf6e0c64..bf10891898 100644 --- a/relayer/src/config.rs +++ b/relayer/src/config.rs @@ -50,7 +50,9 @@ pub mod default { } #[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct Config { + #[serde(default)] pub global: GlobalConfig, #[serde(default)] pub telemetry: TelemetryConfig, @@ -83,26 +85,55 @@ impl Default for Strategy { } } +/// Log levels are wrappers over [`tracing_core::Level`]. +/// +/// [`tracing_core::Level`]: https://docs.rs/tracing-core/0.1.17/tracing_core/struct.Level.html +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +impl Default for LogLevel { + fn default() -> Self { + Self::Info + } +} + +impl fmt::Display for LogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LogLevel::Trace => write!(f, "trace"), + LogLevel::Debug => write!(f, "debug"), + LogLevel::Info => write!(f, "info"), + LogLevel::Warn => write!(f, "warn"), + LogLevel::Error => write!(f, "error"), + } + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] pub struct GlobalConfig { - #[serde(default)] pub strategy: Strategy, - - /// All valid log levels, as defined in tracing: - /// https://docs.rs/tracing-core/0.1.17/tracing_core/struct.Level.html - pub log_level: String, + pub log_level: LogLevel, } impl Default for GlobalConfig { fn default() -> Self { Self { strategy: Strategy::default(), - log_level: "info".to_string(), + log_level: LogLevel::default(), } } } #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct TelemetryConfig { pub enabled: bool, pub host: String, @@ -120,6 +151,7 @@ impl Default for TelemetryConfig { } #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct ChainConfig { pub id: ChainId, pub rpc_addr: tendermint_rpc::Url,